0%

前言

在本篇文章中,我会对Go语言编程模式的一些基本技术和要点,这样可以让你更容易掌握Go语言编程。其中,主要包括,数组切片的一些小坑,还有接口编程,以及时间和程序运行性能相关的话题。

更新历史

2020 年 12 月 18 日 - 初稿

扩展阅读


Slice

首先,我们先来讨论一下Slice,中文翻译叫“切片”,这个东西在Go语言中不是数组,而是一个结构体,其定义如下:

1
2
3
4
5
type slice struct {
array unsafe.Pointer //指向存放数据的数组指针
len int //长度有多大
cap int //容量有多大
}

用图示来看,一个空的slice的表现如下:

img

熟悉C/C++的同学一定会知道,在结构体里用数组指针的问题——数据会发生共享!下面我们来看一下slice的一些操作

1
2
3
4
5
6
foo = make([]int, 5)
foo[3] = 42
foo[4] = 100

bar := foo[1:4]
bar[1] = 99

对于上面这段代码。

  • 首先先创建一个foo的slice,其中的长度和容量都是5
  • 然后开始对foo所指向的数组中的索引为3和4的元素进行赋值
  • 然后,对foo做切片后赋值给bar,再修改bar[1]

img

通过上图我们可以看到,因为foo和bar的内存是共享的,所以,foo和bar的对数组内容的修改都会影响到对方。

接下来,我们再来看一个数据操作 append() 的示例

1
2
3
4
a := make([]int, 32)
b := a[1:16]
a = append(a, 1)
a[2] = 42

上面这段代码中,把 a[1:16] 的切片赋给到了 b ,此时,ab 的内存空间是共享的,然后,对 a做了一个 append()的操作,这个操作会让 a 重新分享内存,导致 ab 不再共享,如下图所示:

img

从上图我们可以看以看到 append()操作让 a 的容量变成了64,而长度是33。这里,需要重点注意一下——append()这个函数在 cap 不够用的时候就会重新分配内存以扩大容量,而如果够用的时候不不会重新分享内存!

我们再看来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {  
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/’)

dir1 := path[:sepIndex]
dir2 := path[sepIndex+1:]

fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

dir1 = append(dir1,"suffix"...)

fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB
}

上面这个例子中,dir1dir2 共享内存,虽然 dir1 有一个 append() 操作,但是因为 cap 足够,于是数据扩展到了dir2 的空间。下面是相关的图示(注意上图中 dir1dir2 结构体中的 caplen 的变化)

img

如果要解决这个问题,我们只需要修改一行代码。

1
dir1 := path[:sepIndex]

修改为

1
dir1 := path[:sepIndex:sepIndex]

新的代码使用了 Full Slice Expression,其最后一个参数叫“Limited Capacity”,于是,后续的 append() 操作将会导致重新分配内存。

深度比较

当我们复杂一个对象时,这个对象可以是内建数据类型,数组,结构体,map……我们在复制结构体的时候,当我们需要比较两个结构体中的数据是否相同时,我们需要使用深度比较,而不是只是简单地做浅度比较。这里需要使用到反射 reflect.DeepEqual() ,下面是几个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (  
"fmt"
"reflect"
)

func main() {

v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2))
//prints: v1 == v2: true

m1 := map[string]string{"one": "a","two": "b"}
m2 := map[string]string{"two": "b", "one": "a"}
fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2))
//prints: m1 == m2: true

s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2))
//prints: s1 == s2: true
}

接口编程

下面,我们来看段代码,其中是两个方法,它们都是要输出一个结构体,其中一个使用一个函数,另一个使用一个“成员函数”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func PrintPerson(p *Person) {
fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",
p.Name, p.Sexual, p.Age)
}

func (p *Person) Print() {
fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",
p.Name, p.Sexual, p.Age)
}


func main() {
var p = Person{
Name: "Hao Chen",
Sexual: "Male",
Age: 44,
}

PrintPerson(&p)
p.Print()
}

你更喜欢哪种方式呢?在 Go 语言中,使用“成员函数”的方式叫“Receiver”,这种方式是一种封装,因为 PrintPerson()本来就是和 Person强耦合的,所以,理应放在一起。更重要的是,这种方式可以进行接口编程,对于接口编程来说,也就是一种抽象,主要是用在“多态”,这个技术,在《Go语言简介(上):接口与多态》中已经讲过。在这里,我想讲另一个Go语言接口的编程模式。

首先,我们来看一下,有下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Country struct {
Name string
}

type City struct {
Name string
}

type Printable interface {
PrintStr()
}
func (c Country) PrintStr() {
fmt.Println(c.Name)
}
func (c City) PrintStr() {
fmt.Println(c.Name)
}

c1 := Country {"China"}
c2 := City {"Beijing"}
c1.PrintStr()
c2.PrintStr()

其中,我们可以看到,其使用了一个 Printable 的接口,而 CountryCity 都实现了接口方法 PrintStr() 而把自己输出。然而,这些代码都是一样的。能不能省掉呢?

我们可以使用“结构体嵌入”的方式来完成这个事,如下的代码所示,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type WithName struct {
Name string
}

type Country struct {
WithName
}

type City struct {
WithName
}

type Printable interface {
PrintStr()
}

func (w WithName) PrintStr() {
fmt.Println(w.Name)
}

c1 := Country {WithName{ "China"}}
c2 := City { WithName{"Beijing"}}
c1.PrintStr()
c2.PrintStr()

引入一个叫 WithName的结构体,然而,所带来的问题就是,在初始化的时候,变得有点乱。那么,我们有没有更好的方法?下面是另外一个解。

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
type Country struct {
Name string
}

type City struct {
Name string
}

type Stringable interface {
ToString() string
}
func (c Country) ToString() string {
return "Country = " + c.Name
}
func (c City) ToString() string{
return "City = " + c.Name
}

func PrintStr(p Stringable) {
fmt.Println(p.ToString())
}

d1 := Country {"USA"}
d2 := City{"Los Angeles"}
PrintStr(d1)
PrintStr(d2)

上面这段代码,我们可以看到——我们使用了一个叫Stringable 的接口,我们用这个接口把“业务类型” CountryCity 和“控制逻辑” Print() 给解耦了。于是,只要实现了Stringable 接口,都可以传给 PrintStr() 来使用。

这种编程模式在Go 的标准库有很多的示例,最著名的就是 io.Readioutil.ReadAll 的玩法,其中 io.Read 是一个接口,你需要实现他的一个 Read(p []byte) (n int, err error) 接口方法,只要满足这个规模,就可以被 ioutil.ReadAll这个方法所使用。这就是面向对象编程方法的黄金法则——“Program to an interface not an implementation”

接口完整性检查

另外,我们可以看到,Go语言的编程器并没有严格检查一个对象是否实现了某接口所有的接口方法,如下面这个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Shape interface {
Sides() int
Area() int
}
type Square struct {
len int
}
func (s* Square) Sides() int {
return 4
}
func main() {
s := Square{len: 5}
fmt.Printf("%d\n",s.Sides())
}

我们可以看到 Square 并没有实现 Shape 接口的所有方法,程序虽然可以跑通,但是这样编程的方式并不严谨,如果我们需要强制实现接口的所有方法,那么我们应该怎么办呢?

在Go语言编程圈里有一个比较标准的作法:

1
var _ Shape = (*Square)(nil)

声明一个 _ 变量(没人用),其会把一个 nil 的空指针,从 Square 转成 Shape,这样,如果没有实现完相关的接口方法,编译器就会报错:

cannot use (Square)(nil) (type Square) as type Shape in assignment: *Square does not implement Shape (missing Area method)

这样就做到了个强验证的方法。

时间

对于时间来说,这应该是编程中比较复杂的问题了,相信我,时间是一种非常复杂的事。而且,时间有时区、格式、精度等等问题,其复杂度不是一般人能处理的。所以,一定要重用已有的时间处理,而不是自己干。

在 Go 语言中,你一定要使用 time.Timetime.Duration 两个类型:

  • 在命令行上,flag 通过 time.ParseDuration 支持了 time.Duration
  • JSon 中的 encoding/json 中也可以把time.Time 编码成 RFC 3339 的格式
  • 数据库使用的 database/sql 也支持把 DATATIMETIMESTAMP 类型转成 time.Time
  • YAML你可以使用 gopkg.in/yaml.v2 也支持 time.Timetime.DurationRFC 3339 格式

如果你要和第三方交互,实在没有办法,也请使用 RFC 3339 的格式。

最后,如果你要做全球化跨时区的应用,你一定要把所有服务器和时间全部使用UTC时间。

性能提示

Go 语言是一个高性能的语言,但并不是说这样我们就不用关心性能了,我们还是需要关心的。下面是一个在编程方面和性能相关的提示。

  • 如果需要把数字转字符串,使用 strconv.Itoa() 会比 fmt.Sprintf() 要快一倍左右
  • 尽可能地避免把String转成[]Byte 。这个转换会导致性能下降。
  • 如果在for-loop里对某个slice 使用 append()请先把 slice的容量很扩充到位,这样可以避免内存重新分享以及系统自动按2的N次方幂进行扩展但又用不到,从而浪费内存。
  • 使用StringBuffer 或是StringBuild 来拼接字符串,会比使用 ++= 性能高三到四个数量级。
  • 尽可能的使用并发的 go routine,然后使用 sync.WaitGroup 来同步分片操作
  • 避免在热代码中进行内存分配,这样会导致gc很忙。尽可能的使用 sync.Pool 来重用对象。
  • 使用 lock-free的操作,避免使用 mutex,尽可能使用 sync/Atomic包。 (关于无锁编程的相关话题,可参看《无锁队列实现》或《无锁Hashmap实现》)
  • 使用 I/O缓冲,I/O是个非常非常慢的操作,使用 bufio.NewWrite()bufio.NewReader() 可以带来更高的性能。
  • 对于在for-loop里的固定的正则表达式,一定要使用 regexp.Compile() 编译正则表达式。性能会得升两个数量级。
  • 如果你需要更高性能的协议,你要考虑使用 protobufmsgp 而不是JSON,因为JSON的序列化和反序列化里使用了反射。
  • 你在使用map的时候,使用整型的key会比字符串的要快,因为整型比较比字符串比较要快。

前言

Istio 1.5 开始支持在数据面支持Wasm扩展,相关规范以及SDK在proxy-wasm,目前提供有三种 SDK 实现,分别是C++RustAssemblyScript,其中AssemblyScriptsolo-io/proxy-runtime。除了 SDK Solo 还发布了 WebAssembly Hub,并配有wasme CLI 工具,为 Wasm 扩展的开发提供了不错体验。本文主要介绍如何使用 Rust SDK 开发 Istio Wasm 扩展。

更新历史

2020 年 11 月 28 日 - 初稿

扩展阅读


Wasme 工具

WebAssembly Hub Getting Started

1
2
3
4
5
$ curl -sL https://run.solo.io/wasme/install | sh
$ export PATH=$HOME/.wasme/bin:$PATH
$ wasme --version

wasme version 0.0.14

Rust 项目

Rust安装

1
2
3
$ rustc --version

rustc 1.43.1 (8d69840ab 2020-05-04)

创建Rust项目

使用lib模板创建项目

1
$ cargo new hello-wold --lib 

编辑Cargo.toml

添加依赖

1
2
3
[dependencies]
log = "0.4.8"
proxy-wasm = "0.1.0" # The Rust SDK for proxy-wasm

配置动态库编译

1
2
3
[lib]
path = "src/lib.rs"
crate-type = ["cdylib"]

Wasm 扩展

源码wasm-hello-world-rust

先通过 Rust SDK 了解扩展功能,编辑src/lib.rs

1
2
3
4
5
6
7
8
9
10
use proxy_wasm as wasm;

struct HelloWorld {
context_id: u32,
}

impl wasm::traits::Context for HelloWorld {}
impl wasm::traits::RootContext for HelloWorld{}
impl wasm::traits::StreamContext for HelloWorld{}
impl wasm::traits::HttpContext for HelloWorld{}

安装依赖

1
$ cargo build

要了解扩展看下几个 Context 的结构,其中on_*开头的便是扩展点,其他get_*set_*add_*等提供了不能接口能力。

为 Request 添加自定义 Header

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
use log::info;
use proxy_wasm as wasm;

#[no_mangle]
pub fn _start() {
proxy_wasm::set_log_level(wasm::types::LogLevel::Trace);
proxy_wasm::set_http_context(
|context_id, _root_context_id| -> Box<dyn wasm::traits::HttpContext> {
Box::new(HelloWorld { context_id })
},
)
}

struct HelloWorld {
context_id: u32,
}

impl wasm::traits::Context for HelloWorld {}

impl wasm::traits::HttpContext for HelloWorld {
fn on_http_request_headers(&mut self, num_headers: usize) -> wasm::types::Action {
info!("Got {} HTTP headers in #{}.", num_headers, self.context_id);
let headers = self.get_http_request_headers();
let mut authority = "";

for (name, value) in &headers {
if name == ":authority" {
authority = value;
}
}

self.set_http_request_header("x-hello", Some(&format!("Hello world from {}", authority)));

wasm::types::Action::Continue
}
}

编译

1
$ cargo build --target wasm32-unknown-unknown --release

runtime-config.jsonwasme build时需要的,可以通过wasme init创建一个cpp项目获得

1
2
3
4
5
6
7
8
9
10
11
{
"type": "envoy_proxy",
"abiVersions": [
"v0-097b7f2e4cc1fb490cc1943d0d633655ac3c522f"
],
"config": {
"rootIds": [
"hello_world"
]
}
}

打包

1
2
3
4
5
$ wasme build precompiled target/wasm32-unknown-unknown/release/hello_world.wasm --tag webassemblyhub.io/amoyw/hello_world:v0.0.1
$ wasme list

NAME TAG SIZE SHA UPDATED
webassemblyhub.io/amoyw/hello_world v0.0.1 1.9 MB 2e95e556 28 Nov 20 14:27 CST

wasme build三种方式,

1
2
3
4
5
6
$ wasme build -h

Available Commands:
assemblyscript Build a wasm image from an AssemblyScript filter using NPM-in-Docker
cpp Build a wasm image from a CPP filter using Bazel-in-Docker
precompiled Build a wasm image from a Precompiled filter.

本地测试 Envoy

1
2
$ wasme deploy envoy webassemblyhub.io/a mo yw/hello_world:v0.0.1 --envoy-image=istio/proxyv2:1.6.0 --bootstrap=envoy-bootstrap.yml
[2020-05-24 03:03:41.736][12][info][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1103] wasm log hello_world hello_world : Got 16 HTTP headers in #9.

http://localhost:8080/headers

1
2
3
4
5
{
"headers" : {
"X-Hello" : "Hello world from localhost:8080"
}
}

wasme deploy三种方式

1
2
3
4
5
6
$ wasme deploy -h

Available Commands:
envoy Run Envoy locally in Docker and attach a WASM Filter.
gloo Deploy an Envoy WASM Filter to the Gloo Gateway Proxies (Envoy).
istio Deploy an Envoy WASM Filter to Istio Sidecar Proxies (Envoy).

WebAssembly Hub 使用

Create a User on webassemblyhub.io & Log In from the wasme command line

Push 镜像

1
$ wasme push webassemblyhub.io/amoyw/hello_world:v0.0.1

发布到 Istio

httpbin 用例自己部署,本例namespace=ns-httpbin

可以分别通过wasme工具或operator添加扩展

CLI 手动添加

1
2
3
$ wasme deploy istio webassemblyhub.io/amoyw/hello_world:v0.0.1 \
--id=hello-world-filter \
--namespace=ns-httpbin

http://{ingress-host}:{port}/headers

1
2
3
4
5
{
"headers" : {
"X-Hello" : "Hello world from {ingress-host}"
}
}

卸载

1
2
3
$ wasme undeploy istio \
--id=hello-world-filter \
--namespace=ns-httpbin

Operator

CRD

1
2
3
$ kubectl apply -f https://github.com/solo-io/wasme/releases/latest/download/wasme.io_v1_crds.yaml

customresourcedefinition.apiextensions.k8s.io/filterdeployments.wasme.io created

Operator

1
2
3
4
5
6
7
8
9
10
11
12
$ kubectl apply -f https://github.com/solo-io/wasme/releases/latest/download/wasme-default.yaml

namespace/wasme created
configmap/wasme-cache created
serviceaccount/wasme-cache created
serviceaccount/wasme-operator created
clusterrole.rbac.authorization.k8s.io/wasme-operator created
clusterrole.rbac.authorization.k8s.io/wasme-cache created
clusterrolebinding.rbac.authorization.k8s.io/wasme-operator created
clusterrolebinding.rbac.authorization.k8s.io/wasme-cache created
daemonset.apps/wasme-cache created
deployment.apps/wasme-operator created

Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
$ kubectl apply -f - <<EOF
apiVersion: wasme.io/v1
kind: FilterDeployment
metadata:
name: hello-world-filter
namespace: ns-httpbin
spec:
deployment:
istio:
kind: Deployment
filter:
image: webassemblyhub.io/amoyw/hello_world:v0.0.1
EOF

Filter 状态

1
2
3
4
5
6
7
$ kubectl get filterdeployments.wasme.io -n ns-httpbin -o yaml hello-world-filter

status:
observedGeneration: "1"
workloads:
httpbin:
state: Succeeded

注意看下 Pod 是否成功,如果READY 1/2可能是istio-proxy没有启动,测试时这里遇到问题wasme-cache经常失败,可能是网络问题,并且失败后不能续传。可以进入 Pod 看缓存的尺寸是否和hello_world.wasm尺寸一致,缓存路径/var/local/lib/wasme-cache/,不一致就等缓存完成后再测试。

http://{ingress-host}:{port}/headers

1
2
3
4
5
{
"headers" : {
"X-Hello" : "Hello world from {ingress-host}"
}
}

卸载

1
$ kubectl delete FilterDeployment -n ns-httpbin hello-world-filter

前言

如果你正在大规模进行 CI/CD,却没有使用 Kubernetes 原生 CD,你可能错过了不少东西。Prow——正如这个希腊语的含义“船头”——一直是使 Kubernetes 成为大规模执行 CI/CD 的优秀平台的强大助力。多年来,它也始终处在原生 Kubernetes CD 的最前沿。

如果你经常混迹于 Kubernetes 上游社区,你一定知道 k8s-ci-bot, 它能帮助管理上游的 PR & Issue,几乎无处不在。今天,我们就来探究一下它背后的项目 Prow。

更新历史

2020 年 10 月 22 日 - 初稿

扩展阅读


关于 Prow

在 Kubernetes、Istio 等知名项目的 Github 仓库中,我们经常会看到 xxx-bot 用户,给 issues 添加标签、合并 PR 。这个机器人账户就是被 Prow 驱动的。

Prow 是 Kubernetes 测试特别兴趣小组的项目,目前是 kubernetes/test-infra 的一部分。Prow 是一个基于 Kubernetes 使用各类事件驱动执行 Job 的 CI/CD 系统。

除了执行 Job ,Prow 还能通过以下方式,实现 GitHub 的自动化功能:

  • 策略配置,权限控制等
  • /label 形式的 chat-ops 命令
  • 自动合并 PR

使用 Prow ,我们可以将研发流程自动化,极大地提升了开发体验。

工作原理

Prow 采用的是微服务架构。核心组件如下:

  • hook 是核心无状态服务,负责监听 Github Webhook 并将其分发到指定的插件
  • plank 是控制器,负责管理作业的生命周期
  • deck 是系统的 Dashboard
  • horologium 用来创建周期型的 Job
  • sinker 定时清理无用的 Job

工作流程:

在 issues 中,评论 /assign @someone 。Github 通过 Webhook 将该事件发送给 Prow 。事件到达 hook 组件,再传给各个 PlugIn 。 PlugIn 通过解析事件的 body 数据,判断是否需要创建 ProwJob 。Prowjob 是一个 Job 的 CRD 。最终执行 Job ,将相关的内容指派给 someone ,将文本转换为行为。

关于插件:

prow/plugins 仓库中,我们可以找到一些内置的插件。当然,我们也可以通过扩展插件定制 Prow 的行为。

在 Kubernetes 集群部署 Prow

  • 准备机器人账户

在生产环境,通常会使用一个类似 xxx-bot 的账户专用于 Prow 的行为,以区分人的操作。如果仅是测试,使用个人账户也可以。

将机器人账户添加为仓库管理员。

  • 生成用于 Github 访问的 token

登陆机器人账户,在 settings/tokens 页面,新建一个 token: xxxTokenxxx ,勾选 repo:status 和 public_repo 权限。

  • 使用 Github token 创建集群 secret
1
2
echo "xxxTokenxxx" > oauth-token
kubectl create secret generic oauth-token --from-file=oauth=./oauth-token
  • 在集群生成 hmac ,用于 Github 的 Webhook 认证
1
2
openssl rand -hex 20 > hmac-token
kubectl create secret generic hmac-token --from-file=hmac=./hmac-token

查看 hmac 值,将用于 Github 中 Webhook 的配置。

1
2
3
cat ./hmac-token

xxxHmacxxx
  • 部署 Prow
1
kubectl apply -f https://raw.githubusercontent.com/kubernetes/test-infra/master/prow/cluster/starter.yaml
  • 查看全部 Pod 是否全部 Running
1
kubectl get pod
  • 查看服务访问的端口
1
2
3
4
5
6
7
kubectl get svc

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
deck NodePort 10.233.52.176 <none> 80:32284/TCP 16m
hook NodePort 10.233.30.53 <none> 8888:30381/TCP 16m
kubernetes ClusterIP 10.233.0.1 <none> 443/TCP 4h31m
tide NodePort 10.233.26.103 <none> 80:30855/TCP 16m

这里由于仅用于测试,没有配置 Ingress ,下面会以 ServiceIp + NodePort 的形式进行配置,其中 ServiceIp 为部署的主机 IP 。

  • 查看页面

访问 deck 组件提供的 Dashboard :http://ServiceIP:32284/

新仓库配置

  • 给 Github 仓库添加 Webhook 配置

Payload URL 需要带上 /hook 路由。Content Type 需要选择 application/json 。Secret 是上面生成的 xxxHmacxxx 值。

  • 给 Github 仓库添加 OWNERS 文件

OWNERS 文件用于申明模块的 approvers 和 reviewers ,在 PR 流程中会用到。每个目录都可以使用 OWNERS 进行控制,这里在仓库根目录下添加该文件并提交。其中的 someone 用户,将可以通过评论 /lgtm ,合并 PR 。这里 Prow 中定义的规则是,同时存在 /lgtm 和 /approve 标签时,PR 将会被合并。但是存在一个特例,approver 可以省略 /approve 而直接使用 /lgtm 合并 PR。

OWNERS 文件:

1
2
3
4
5
approvers:
- someone

reviewers:
- someone
  • 启用指定插件

创建插件描述文件 plugins.yaml ,这里以 xuecanlong/prow-test 仓库为例。如果是多个仓库,可以按照 yaml 语法,列在 plugins 下。

plugins.yaml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
plugins:
xuecanlong/prow-test:
- size
- lgtm
- approve
- label
- trigger
- hold
- verify-owners
- wip
- milestone
- welcome
- heart
- help
- assign

执行命令:

1
2
3
kubectl create configmap plugins \
--from-file=plugins.yaml=./plugins.yaml --dry-run -o yaml \
| kubectl replace configmap plugins -f -
  • 配置 Tide

Tide 用于 PR 的合并,多个仓库可以按照 yaml 语法新增在 repos 字段下。

config.yaml

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
tide:
merge_method:
kubeflow/community: squash

target_url: http://ServiceIp:30855/tide
queries:
- repos:
- xuecanlong/prow-test
labels:
- lgtm
- approved
missingLabels:
- do-not-merge
- do-not-merge/hold
- do-not-merge/work-in-progress
- needs-ok-to-test
- needs-rebase

context_options:
from-branch-protection: true
skip-unknown-contexts: true
orgs:
org:
required-contexts:
- "check-required-for-all-repos"
repos:
repo:
required-contexts:
- "check-required-for-all-branches"
branches:
branch:
from-branch-protection: false
required-contexts:
- "required_test"
optional-contexts:
- "optional_test"

执行命令:

1
kubectl create configmap config --from-file=config.yaml=./config.yaml --dry-run -o yaml | kubectl replace configmap config -f -
  • 定制标签(可选)

通过 /xxx xxx 评论,能够给 issues 或 PR 添加标签。前提是仓库中已经新建了相关的二维标签。 如果需要定制标签,可以参考 内置标签 定义自己的 labels.yaml 。

执行命令:

1
2
3
kubectl create configmap label-config \
--from-file=labels.yaml=labels.yaml --dry-run -o yaml \
| kubectl replace configmap config -f -
  • bazel 和 add-hook 工具

克隆 Prow 的代码仓库 kubernetes/test-infra/prow ,使用 bazel 可以检测配置是否正确

1
bazel run prow/cmd/checkconfig -- --plugin-config=path/to/plugins.yaml --config-path=path/to/config.yaml

kubernetes/test-infra/experiment 中提供了一个 add-hook 工具,可以免去在 Github 页面配置 Webhook 。

1
add-hook  --hmac-path=h-mac --github-token-path=oauth -hook-url http://ServiceIp:30381/hook -repo xuecanlong/prow-test -confirm=true

前言

在某些情况下,我们需要模拟网络很差的状态来测试软件能够正常工作,比如网络延迟、丢包、乱序、重复等。Linux 系统下强大的流量控制工具 TC 能很轻松地完成这个需求,TC 命令行工具是 IProute2 软件包中的软件,可以根据系统版本自行安装。

这篇文章介绍的功能主要是通过 Netem 这个内核模块来实现的。NetemNetwork Emulator 的缩写,关于更多功能以及参数的详细解释可以参阅 TC-Netem 的 Man Page。

Netem 与 TC 简要说明

Netem 是 Linux 2.6 及以上内核版本提供的一个网络模拟功能模块。该功能模块可以用来在性能良好的局域网中,模拟出复杂的互联网传输性能。例如:低带宽、传输延迟、丢包等等情况。使用 Linux 2.6 (或以上) 版本内核的很多 Linux 发行版都默认开启了该内核模块,比如:Fedora、Ubuntu、Redhat、OpenSuse、CentOS、Debian 等等。

TC 是 Linux 系统中的一个用户态工具,全名为 Traffic Control (流量控制)。TC 可以用来控制 Netem 模块的工作模式,也就是说如果想使用 Netem 需要至少两个条件,一是内核中的 Netem 模块被启用,另一个是要有对应的用户态工具 TC 。

TC 能做的事情很多,除了本文介绍的还有带宽控制、优先级控制等等。这些功能是通过类似 Netem 的内核模块实现的。

更新历史

2020 年 03 月 03 日 - 初稿

扩展阅读

http://t.cn/RsUAV3y
http://t.cn/RsUwmQX
http://t.cn/RsUVqk0


网络状况模拟

网络状况欠佳从用户角度来说就是下载东西慢(网页一直加载、视频卡顿、图片加载很久等),从网络报文角度来看却有很多情况:比如:延迟(某个机器发送报文很慢)、丢包(发送的报文在网络中丢失需要一直重传)、乱序(报文顺序错乱,需要大量计算时间来重新排序)、重复(报文有大量重复,导致网络拥堵)、错误(接收到的报文有误只能丢弃重传)等。

对于这些情况,都可以用 Netem 来模拟。需要注意的是,Netem 是直接作用于指定网卡上的,也就是说所有从该网卡发送出去的包都会收到配置参数的影响,所以最好搭建临时的虚拟机进行测试。

在下面的例子中 add 表示为指定网卡添加 Netem 配置,change 表示修改已经存在的 Netem 配置到新的值,replace 表示替换已经存在的 Netem 配置的值。如果要删除网卡上的 Netem 配置可以使用 del

1
$ tc qdisc del dev eth0 root

1. 模拟延迟传输

最简单的例子是所有的报文延迟 100ms 发送:

1
$ tc qdisc add dev eth0 root netem delay 100ms

如果你想在一个局域网里模拟远距离传输的延迟可以用这个方法,比如实际用户访问网站延迟为 101 ms,而你测试环境网络交互只需要 1ms,那么只要添加 100ms 额外延迟就行。

1
2
3
4
5
6
7
8
9
10
$ tc qdisc replace dev eth0 root netem delay 100ms
$ ping dev-node-02
PING dev-node-02 (192.168.100.212) 56(84) bytes of data.
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=1 ttl=64 time=102 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=2 ttl=64 time=100 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=3 ttl=64 time=100 ms
^C
--- dev-node-02 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3000ms
rtt min/avg/max/mdev = 100.293/101.053/102.795/1.061 ms

如果在网络中看到非常稳定的时延,很可能是某个地方加了定时器,因为网络线路很复杂,传输过程一定会有变化。因此实际情况网络延迟一定会有变化的,Netem 也考虑到这一点,提供了额外的参数来控制延迟的时间分布。完整的参数列表为:

1
2
DELAY := delay TIME [ JITTER [ CORRELATION ]]]
[ distribution { uniform | normal | pareto | paretonormal } ]

除了延迟时间 TIME 之外,还有三个可选参数:

  • JITTER:抖动,增加一个随机时间长度,让延迟时间出现在某个范围。
  • CORRELATION:相关,下一个报文延迟时间和上一个报文的相关系数。
  • distribution:分布,延迟的分布模式。可以选择的值有 uniformnormalparetoparetonormal

先说说 JITTER,如果设置为 20ms,那么报文延迟的时间在 100ms ± 20ms 之间,具体值随机选择:

1
2
3
4
5
6
7
$ tc qdisc replace dev eth0 root netem delay 100ms 20ms
$ ping dev-node-02
PING dev-node-02 (192.168.100.212) 56(84) bytes of data.
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=1 ttl=64 time=108 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=2 ttl=64 time=107 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=3 ttl=64 time=92 ms
......

CORRELATION 指相关性,因为网络状况是平滑变化的,短时间里相邻报文的延迟应该是近似的而不是完全随机的。这个值是个百分比,如果为 100%,就退化到固定延迟的情况;如果是 0% 则退化到随机延迟的情况。

1
2
3
4
5
6
7
$ tc qdisc replace dev eth0 root netem delay 100ms 20ms 50%
$ ping dev-node-02
PING dev-node-02 (192.168.100.212) 56(84) bytes of data.
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=1 ttl=64 time=104 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=2 ttl=64 time=109 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=5 ttl=64 time=101 ms
......

报文的分布和很多现实事件一样都满足某种统计规律,比如最常用的正态分布。因此为了更逼近现实情况,可以使用 distribution 参数来限制它的延迟分布模型。比如让报文延迟时间满足正态分布:

1
2
3
4
5
6
7
$ tc qdisc replace dev eth0 root netem delay 100ms 20ms distribution normal
$ ping dev-node-02
PING dev-node-02 (192.168.100.212) 56(84) bytes of data.
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=1 ttl=64 time=82.0 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=2 ttl=64 time=82.3 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=3 ttl=64 time=98.1 ms
......

这样的话,大部分的延迟会在平均值的一定范围内,而很少接近出现最大值和最小值的延迟。

其他分布方法包括:uniform)、paretoparetonormal,这些分布方法感兴趣的读者可以自行了解。对于大多数情况,随机在某个时间范围里延迟就能满足需求的。

2. 模拟丢包率

另一个常见的网络异常是因为丢包,丢包会导致重传,从而增加网络链路的流量和延迟。Netem 的 loss 参数可以模拟丢包率,比如发送的报文有 50% 的丢包率(为了容易用 ping 看出来,所以这个数字我选的很大,实际情况丢包率可能比这个小很多,比如 0.5%):

1
2
3
4
5
6
7
8
$ tc qdisc change dev eth0 root netem loss 50%
$ ping dev-node-02
PING dev-node-02 (192.168.100.212) 56(84) bytes of data.
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=1 ttl=64 time=0.290 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=4 ttl=64 time=0.308 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=5 ttl=64 time=0.221 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=8 ttl=64 time=0.371 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=9 ttl=64 time=0.315 ms

可以从 icmp_seq 序号看出来大约有一半的报文丢掉了,和延迟类似丢包率也可以增加一个相关系数,表示后一个报文丢包概率和它前一个报文的相关性。

1
$ tc qdisc change dev eth0 root netem loss 0.3% 25%

这个命令表示,丢包率是 0.3%,并且当前报文丢弃的可能性和前一个报文有 25% 相关。默认的丢包模型为随机,loss 也支持 state(4-state Markov 模型) 和 gemodel(Gilbert-Elliot 丢包模型) 两种模型的丢包,因为两者都相对复杂,这里就不再详细介绍了。

需要注意的是,丢包信息会发送到上层协议。如果是 TCP 协议,那么 TCP 会进行重传,所以对应用来说看不到丢包。这时候要模拟丢包,需要把 loss 配置到网桥或者路由设备上。

3. 模拟包重复

报文重复和丢包的参数类似,就是重复率和相关性两个参数,比如随机产生 50% 重复的包:

1
2
3
4
5
6
7
8
9
$ tc qdisc change dev eth0 root netem duplicate 50%
$ ping dev-node-02
PING dev-node-02 (192.168.100.212) 56(84) bytes of data.
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=2 ttl=64 time=0.284 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=3 ttl=64 time=0.420 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=3 ttl=64 time=0.447 ms (DUP!)
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=4 ttl=64 time=0.437 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=4 ttl=64 time=0.515 ms (DUP!)
......

4. 模拟包损坏

报文损坏和报文重复的参数也类似,比如随机产生 2% 损坏的报文(在报文的随机位置造成一个比特的错误)。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ tc qdisc change dev eth0 root netem corrupt 2%
$ ping dev-node-02
......
PING dev-node-02 (192.168.100.212) 56(84) bytes of data.
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=3 ttl=64 time=0.362 ms
Warning: time of day goes back (-4611686018427387574us), taking countermeasures.
Warning: time of day goes back (-4611686018427387454us), taking countermeasures.
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=4 ttl=64 time=0.000 ms
wrong data byte #53 should be 0x35 but was 0xb5
#16 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f
#48 30 31 32 33 34 b5 36 37
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=5 ttl=64 time=0.476 ms
......

5. 模拟包乱序

网络传输并不能保证顺序,传输层 TCP 会对报文进行重组保证顺序,所以报文乱序对应用的影响比上面的几种问题要小。

报文乱序和前面的参数不太一样,因为上面的报文问题都是独立的。针对单个报文做操作就行,而乱序则牵涉到多个报文的重组。模拟报乱序一定会用到延迟(因为模拟乱序的本质就是把一些包延迟发送),Netem 有两种方法可以做。

第一种是固定的每隔一定数量的报文就乱序一次。

1
2
3
4
5
6
7
8
9
10
11
12
# 每 5 个报文(第 5、10、15…报文)会正常发送,其他的报文延迟 100ms。
$ tc qdisc change dev eth0 root netem reorder 50% gap 3 delay 100ms
$ ping -i 0.05 dev-node-02
PING dev-node-02 (192.168.100.212) 56(84) bytes of data.
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=9 ttl=64 time=2.55 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=8 ttl=64 time=100 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=10 ttl=64 time=100 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=11 ttl=64 time=100 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=13 ttl=64 time=0.245 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=12 ttl=64 time=102 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=14 ttl=64 time=1.00 ms
......

要想看到 ping 报文的乱序,我们要保证发送报文的间隔小于报文的延迟时间 100ms,这里用 -i 0.05 把发送间隔设置为 50ms

第二种方法的乱序是相对随机的,使用概率来选择乱序的报文。

1
2
3
4
5
6
7
8
9
10
$ tc qdisc change dev eth0 root netem reorder 50% 15% delay 300ms
$ ping -i 0.05 dev-node-02
PING dev-node-02 (192.168.100.212) 56(84) bytes of data.
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=4 ttl=64 time=0.423 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=7 ttl=64 time=0.250 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=2 ttl=64 time=301 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=3 ttl=64 time=301 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=9 ttl=64 time=0.238 ms
64 bytes from dev-node-02 (192.168.100.212): icmp_seq=5 ttl=64 time=301 ms
......

50% 的报文会正常发送,其他报文(1-50%)延迟 300ms 发送,这里选择的延迟很大是为了能够明显看出来乱序的结果。

6. 其它技巧

  • 查看已经配置的网络条件

该命令将查看并显示 eth0 网卡的相关传输配置。

1
$ tc qdisc show dev eth0

推荐两个工具

Netem 在 TC 中算是比较简单的模块,如果要实现流量控制或者精细化的过滤需要更复杂的配置。这里推荐两个小工具,它们共同的特点是用法简单,能满足特定的需求,而不用自己去倒腾 TC 的命令。

1. Wondershaper

项目地址:https://github.com/magnific0/wondershaper

Netem 只能模拟网络状况,不能控制带宽,Wondershaper 则能完美解决这个问题。Wondershaper 实际上是一个 SHELL 脚本,它使用 TC 来进行流量速率调整,使用 QoS 来处理特定的网络接口。外发流量通过放在不同优先级的队列中,来达到限制传出流量速率的目的;而传入流量通过丢包的方式来达到速率限制的目的。

安装 Wondershaper

  • 在 Ubuntu / Debian 下安装 Wondershaper
1
$ sudo apt-get install wondershaper
  • 在 Fdora / CentOS / RHEL 中安装 Wondershaper
1
2
# 需启用 EPEL 仓库
$ sudo yum install wondershaper

使用 Wondershaper

Wondershaper 的使用非常简单,只有三个参数:网卡名、下行限速、上行限速。比如要设置网卡下载速度为 200kb/s,上传速度为 150kb/s

1
$ sudo wondershaper eth0 200 150

如果你要将速率限制消除,可以通过运行下面的命令来达到目的。

1
$ sudo wondershaper clear eth0

2. Comcast

项目地址:https://github.com/tylertreat/comcast

Comcast 是一个跨平台的网络模拟工具,旨在其他平台(OSX、Windows、BSD)也能提供类似网络模拟的功能。

它的使用也相对简单:

1
2
3
4
5
6
7
$ comcast --device=eth0 --latency=250 \
--target-bw=1000 \
--default-bw=1000000 \
--packet-loss=10% \
--target-addr=8.8.8.8,10.0.0.0/24 \
--target-proto=tcp,udp,icmp \
--target-port=80,22,1000:2000
  • --device 说明要控制的网卡为 eth0
  • --latency 指定 250ms 的延迟。
  • --target-bw指定目标带宽。
  • --default-bw 指定默认带宽。
  • --packet-loss 指定丢包率。
  • --target-addr--target-proto--target-port 参数指定在满足这些条件的报文上实施上面的配置。

总结

可以看出,TC 的 Netem 模块主要用来模拟各种网络的异常状况,本身并没有提供宽带限制的功能,而且一旦在网卡上配置了 Netem,该网卡上所有的报文都会受影响,如果想精细地控制部分报文,需要用到 TC 的 filter 功能。

前言

Codis 3.x 稳定版本已经很久没更新了,虽然有缺点也称不上完美但确实可以有效解决横向扩展问题。Redis 5.0 因为所谓的 政治正确 把 master-slave 名字修改为 master-replica 上了开源社区热议排行榜,然而大家在选择 Redis 集群方案的时候除了自研和 Codis 以外依然没有太多的选择余地。我们使用 Codis 的原因也很简单,Redis 主从模式内存从 128GB 一路增加到 1TB 后硬件终于受不鸟了,要么像数据库借鉴 “拆” 的奥义做到庖丁解牛一般,不然摆在眼前的路基本只剩下相对成熟可靠的 Codis。本文分享了 Redis 高可用技术解决方案选型的参考文章和 Codis 集群搭建的过程,希望对大家有帮助。

Redis(Codis)分布式集群部署实践

更新历史

2019 年 07 月 23 日 - 初稿

扩展阅读

Redis
Codis


Codis 简介

Codis 是一个分布式 Redis 解决方案, 对于上层的应用来说, 连接到 Codis Proxy 和连接原生的 Redis Server 没有显著区别 (不支持的命令列表), 上层应用可以像使用单机的 Redis 一样使用, Codis 底层会处理请求的转发, 不停机的数据迁移等工作, 所有后边的一切事情, 对于前面的客户端来说是透明的, 可以简单的认为后边连接的是一个内存无限大的 Redis 服务。

Compared with Twemproxy and Redis Cluster

Codis Twemproxy Redis Cluster
resharding without restarting cluster Yes No Yes
pipeline Yes Yes No
hash tags for multi-key operations Yes Yes Yes
multi-key operations while resharding Yes - No(details)
Redis clients supporting Any clients Any clients Clients have to support cluster protocol

“Resharding” means migrating the data in one slot from one redis server to another, usually happens while increasing/decreasing the number of redis servers.

为什么要选择 Codis

Redis 获得动态扩容 / 缩容的能力,增减 redis 实例对 client 完全透明、不需要重启服务,不需要业务方担心 Redis 内存爆掉的问题. 也不用担心申请太大, 造成浪费. 业务方也不需要自己维护 Redis.

Codis 支持水平扩容 / 缩容,扩容可以直接界面的 “Auto Rebalance” 按钮,缩容只需要将要下线的实例拥有的 slot 迁移到其它实例,然后在界面上删除下线的 group 即可。

Codis 使用文档
Codis FAQ
Codis 不支持的命令列表
redis 修改部分(增加若干指令)
Performance (Benchmark)

Codis 架构

img

集群配置前需要了解架构,集群分片主要分三种:

  1. 客户端分片:这个需要自己开发,对客户端要求严格,集群很难扩容
  2. 代理端分片:如 codis,对客户端几乎无要求,集群容易扩容
  3. 服务端分片:如 redis 集群,需要智能客户端支持集群协议的,集群容易扩容

Codis 3.x 由以下组件组成:

  • Codis Server:基于 redis-3.2.8 分支开发。增加了额外的数据结构,以支持 slot 有关的操作以及数据迁移指令。具体的修改可以参考文档 redis 的修改
  • Codis Proxy:客户端连接的 Redis 代理服务, 实现了 Redis 协议。 除部分命令不支持以外(不支持的命令列表),表现的和原生的 Redis 没有区别(就像 Twemproxy)。
    • 对于同一个业务集群而言,可以同时部署多个 codis-proxy 实例;
    • 不同 codis-proxy 之间由 codis-dashboard 保证状态同步。
  • Codis Dashboard:集群管理工具,支持 codis-proxy、codis-server 的添加、删除,以及据迁移等操作。在集群状态发生改变时,codis-dashboard 维护集群下所有 codis-proxy 的状态的一致性。
    • 对于同一个业务集群而言,同一个时刻 codis-dashboard 只能有 0 个或者 1 个;
    • 所有对集群的修改都必须通过 codis-dashboard 完成。
  • Codis Admin:集群管理的命令行工具。
    • 可用于控制 codis-proxy、codis-dashboard 状态以及访问外部存储。
  • Codis FE:集群管理界面。
    • 多个集群实例共享可以共享同一个前端展示页面;
    • 通过配置文件管理后端 codis-dashboard 列表,配置文件可自动更新。
  • Storage:为集群状态提供外部存储。
    • 提供 Namespace 概念,不同集群的会按照不同 product name 进行组织;
    • 目前仅提供了 Zookeeper、Etcd、Fs 三种实现,但是提供了抽象的 interface 可自行扩展。

Codis 部署

Codis 官方的 GitHub 教程已经写的比较详细了,这里重点分享 Ansible 自动化部署方案

Codis HA

Codis 的架构本身分成 Proxy 集群 + Redis 集群,Proxy 集群的高可用,可以基于 Zookeeper 来做故障转移,而 Redis 集群的高可用是借助于 Redis Sentinel 开源的哨兵集群来实现,那边 Codis 作为非 Redis 组件,需要解决的一个问题就是如何集成 Redis 哨兵集群。

Codis 监控

Redis 迁移至 Codis

分两种情况:

  1. 原来使用 twemproxy 的用户: 可以, 使用 codis 项目内的 redis-port 工具, 可以实时的同步 twemproxy 底下的 redis 数据到你的 codis 集群上. 搞定了以后, 只需要你修改一下你的配置, 将 twemproxy 的地址改成 codis 的地址就好了. 除此之外, 你什么事情都不用做.
  2. 原来使用 Redis 的用户: 如果你使用了 doc/unsupported_cmds中提到的命令,是无法直接迁移到 Codis 上的. 你需要修改你的代码, 用其他的方式实现.

先搭建好 codis 集群并让 codis-proxy 正确运行起来。对线上每一个 redis 实例运行一个 redis-port 来向 codis 导入数据,例如:

1
2
3
4
5
6
for port in {6379,6380,6479,6480}; do
nohup redis-port sync --ncpu=4 --from=redis-server:${port} \
--target=codis-proxy:19000 > ${port}.log 2>&1 &
sleep 5
done
tail -f *.log
  • 每个 redis-port 负责将对应的 redis 数据导入到 codis
  • 多个 redis-port 之间不互相干扰,除非多个 redis 上的 key 本身出现冲突
  • 单个 redis-port 可以将负责的数据并行迁移以提高速度,通过 –ncpu 来指定并行数
  • 导入速度受带宽以及 codis-proxy 处理速度限制(本质是大量的 slotsrestore 操作)
  • 完成数据迁移,在适当的时候将服务指向 Codis,并将原 redis 下线,旧 redis 下线时,会导致 reids-port 链接断开,于是自动退出

redis-port
RedisShake
redis-rdb-tools

Codis 扩容

Codis 可以实现在线不停服务进行扩容,具体的步骤如下:

  1. 安装配置 codis-server 主从
  2. 打开 codis 管理界面,新建 server group 并添加刚刚安装的 redis 实例(注意:codis 默认第一个添加的是 master)
  3. 规划 slot 分布,把部分 slot 迁移到新的 server group 中

备注说明

  1. slot 迁移的过程中,Codis 服务可以正常访问,codis 的迁移机制可以保证数据的一致性
  2. 迁移时,key 都是单个进行迁移,并且不能同时运行多个迁移任务,所以 codis 的迁移时间会比较长。一定要在扩容前留有足够的时间和空间。

Codis 其他经验分享

  1. 关于 HOT KEY, HOT KEY 很影响 Codis/Redis 的性能,这点如果你监控不到位,你就得花一些力气去找到底是哪组出了问题,再 monitor 看看找出是哪个应用干的,比较费时费力,所以在交付 rd 上线时, 我们就严肃声明坚决不允许存在 HOT KEY,宁可使用笨方法多消耗一些内存,也要降低线上故障的风险。
  2. 关于 BIG KEY, 这点风险更为巨大:

由于 Codis 支持 “resharding without restarting cluster”,如果迁移失败,所导致的后果也是不可简单衡量的。Redis 是串行提供服务的,所以当迁移该 BIG KEY 时,其他的请求就会被 BLOCK 住,这点是十分危险的,访问该组的请求皆会失败。

由于 Codis-ha 也会依赖该节点的返回来判断 Codis-server 是否挂掉,如果无响应超过设置时间,便会强制提升 SLAVE 至 MASTER,导致整个迁移任务失败。这时如果 Proxy 的信息没有更新的话,并且迁移故障的 KEY 所在 SLOT 可能会存在 KEY 的信息不完整,虽然服务恢复,但是仍有大量 key 失效。

所以一般不推荐使用 Codis 存大的 HASH 表,LIST 等等,并且在迁移之前,至少要对该 Group 做一次检查 BIG KEY 即:redis-cli –bigkeys 查看是否有 BIG KEY 存在,再酌情迁移。

  1. 关于 Codis-server,一般 Codis-proxy 或者 Codis-dashboard 我们使用 supervisor 管理,在进程退出的情况下立即拉起来重新服务,而 Codis-Server 则不推荐使用该方式,原因是这样的:一般作为 Codis-server,是关闭 rdb dump 的,如果 Codis-server 挂掉,当重新启动时,是没有 rdb 文件的,或者 rdb 文件是上一次切换之前的。如果挂掉立即重新启动,则该 Codis 有可能是空的,或者数据不是最新,而同时,SLAVE 同步,也会清空数据库,或者同步旧数据。

参考文献

史上最全 Redis 高可用技术解决方案大全
深入浅出百亿请求高可用 Redis (codis) 分布式集群揭秘
大规模 codis 集群的治理与实践
避免 Redis (Codis) 的 Timeout 及监控

1.ELK的背景介绍与应用场景

在项目应用运行的过程中,往往会产生大量的日志,我们往往需要根据日志来定位分析我们的服务器项目运行情况与BUG产生位置。一般情况下直接在日志文件中tailf、 grep、awk 就可以获得自己想要的信息。但在规模较大的场景中,此方法效率低下,面临问题包括日志量过大、文本搜索太慢、如何多维度查询。这就需要对服务器上的日志收集汇总。常见解决思路是建立集中式日志收集系统,将所有节点上的日志统一收集,管理,访问。

一般大型系统往往是一种分布式部署的架构,不同的服务模块部署在不同的服务器上,问题出现时,大部分情况需要根据问题暴露的关键信息,定位到具体的服务器和服务模块,所以构建一套集中式日志系统,可以提高定位问题的效率。一个完整的集中式日志系统,需要包含以下几个主要特点:

  • 收集-能够采集多种来源的日志数据,服务日志与系统日志。
  • 传输-能够稳定的把日志数据传输到中央系统
  • 存储-如何存储日志数据,持久化数据。
  • 分析-可以支持 UI 分析,界面化定制查看日志操作。

ELK提供了一整套解决方案,并且都是开源软件,之间互相配合使用,完美衔接,高效的满足了很多场合的应用。是目前主流的一种日志系统。

2.ELK简介:

ELK是三个开源软件的缩写,分别表示:Elasticsearch, Logstash, Kibana , 它们都是开源软件。其中后来新增了一个FileBeat。

即ELK主要由Elasticsearch(搜索)、Logstash(收集与分析)和Kibana(展示)三部分组件组成;

其中各组件说明如下:

Filebeat:轻量级数据收集引擎。早期的ELK架构中使用Logstash收集、解析日志,但是Logstash对内存、cpu、io等资源消耗比较高。如果用它来对服务器进行日志收集,将加重服务器的负载。相比 Logstash,Beats所占系统的CPU和内存几乎可以忽略不计,所以filebeat作为一个轻量级的日志收集处理工具(Agent),它可以用来替代Logstash,由于其占用资源少,所以更适合于在各个服务器上搜集日志后传输给Logstash,这也是官方推荐的一种做法。【收集日志】

Logstash:数据收集处理引擎。支持动态的从各种数据源搜集数据,并对数据进行过滤、分析、丰富、统一格式等操作,然后存储以供后续使用。【对日志进行过滤、分析】

Elasticsearch:分布式搜索引擎。是基于Lucene的开源分布式搜索服务器,具有高可伸缩、高可靠、易管理等特点。可以用于全文检索、结构化检索和分析,并能将这三者结合起来。【搜集、分析、存储数据】

Kibana:可视化平台。它能够搜索、展示存储在 Elasticsearch 中索引数据。使用它可以很方便的用图表、表格、地图展示和分析数据。【图形化展示日志】

结合以上,常见的日志系统架构图如下:

如上图所示,日志文件分别由filebeat在服务器上进行收集,收集的日志文件汇总到logstash上并对文件数据进行过滤、分析、丰富、统一格式等操作,然后发送到Elasticsearch,进一步对日志进行结构化检索和分析,并存储下来,最后由kibana进行展示。

这只是日志系统的一种最初级结构,生产环境中需要对此结构进行进一步的优化。

3.ELK系统的部署(配置将在生产环境架构中进行说明)

环境:CentOS7.5部署ELK 7版本

准备工作:CentOS7.5

elasticsearch7, logstash7, kibana7

关闭防火墙和SELinux并更新yum源(非必须):yum -y update

本次分布式部署的ELK版本为2019年五月份左右最新发布的版本,更新了许多新特性,后面将详细说明。在安装上面本次极大简化了安装步骤,可以源码,组件安装,本次采用YUM+插件docker安装,能达到相同的效果。

3.1Filebeat安装与配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# 安装Filebeat
rpm --import https://packages.elastic.co/GPG-KEY-elasticsearch
cat>/etc/yum.repos.d/elk-elasticsearch.repo<<EOF
[elastic-7.x]
name=Elasticrepository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md
EOF
yum -yinstall filebeat
systemctl enablefilebeat
systemctl startfilebeat

3.2 整套软件:elasticsearch,Logstash,Kibana和Filebeat的安装(分开部署请分别安装三个组件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1. java环境(7版本自带Java)
yum -yinstall java
2. 导入elasticsearch PGP key文件
rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
3. 配置yum源
vim/etc/yum.repos.d/elasticsearch.repo
[elasticsearch-7.x]
name=Elasticsearchrepository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md
4. yum -yinstall elasticsearch logstash kibana
##########关于安装环境,也可以参照以下二种方式,也比较简便
#################----即通过RPM包的方式,此处给出Filebeat的方式,其他组件雷同
curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.0.0-x86_64.rpm
sudo rpm -vifilebeat-7.0.0-x86_64.rpm

----Logstash实例另一种方式(优先采用)
wget https://artifacts.elastic.co/downloads/logstash/logstash-7.0.0.rpm
yum install -y java
yum install -y logstash-7.0.0.rpm

服务启动前必看,由于配置文件在下一章节给出,此处是前提准备,等配置完成后再启动

3.3 关于elasticsearch需要重点关注的地方

1.elasticsearch在分词方面,需要添加中文分词的插件。在其安装代码的plugins目录,即/usr/share/elasticsearch/plugins,需要增加中文分词插件。

1
2
3
4
5
[root@192-168-108-35plugins]# ll
total 3192
[root@192-168-108-35 plugins]# pwd
/usr/share/elasticsearch/plugins
[root@192-168-108-35 plugins]# wget https://codeload.github.com/medcl/elasticsearch-analysis-ik/tar.gz/v7.0.0

下载后重启时需要删除插件源文件,否则报错如下:

1
2
3
Caused by: java.nio.file.FileSystemException:
/usr/share/elasticsearch/plugins/v7.0.0/plugin-descriptor.properties: Not adirectory
所以安装完成后必须删除源文件v7.0.0

2.elasticsearch服务的启动问题与前提准备

  • – 首先在安装完毕后会生成很多文件,包括配置文件日志文件等等,下面几个是最主要的配置文件路径
1
2
3
4
5
/etc/elasticsearch/elasticsearch.yml # els的配置文件
/etc/elasticsearch/jvm.options # JVM相关的配置,内存大小等等
/etc/elasticsearch/log4j2.properties # 日志系统定义
/usr/share/elasticsearch # elasticsearch 默认安装目录
/var/lib/elasticsearch # 数据的默认存放位置
  • – 创建用于存放数据与日志的目录

数据文件会随着系统的运行飞速增长,所以默认的日志文件与数据文件的路径不能满足我们的需求,那么手动创建日志与数据文件路径

1
2
mkdir -p /data/elkdata
mkdir -p /data/elklogs
  • – JVM配置 (7.0版本针对此做出优化,可以基本保障溢出问题,但最好设置一下)

由于Elasticsearch是Java开发的,所以可以通过/etc/elasticsearch/jvm.options配置文件来设定JVM的相关设定。如果没有特殊需求按默认即可。
不过其中还是有两项最重要的-Xmx1g与-Xms1gJVM的最大最小内存。如果太小会导致Elasticsearch刚刚启动就立刻停止。太大会拖慢系统本身。
vim /etc/elasticsearch/jvm.options #JVM最大、最小使用内存

-Xms1g

-Xmx1g

  • – 使用ROOT账户执行命令

elasticsearch的相关配置已经完成,下面需要启动elasticsearch集群。但是由于安全的考虑,elasticsearch不允许使用root用户来启动,所以需要创建一个新的用户,并为这个账户赋予相应的权限来启动elasticsearch集群。

创建ES运行用户

1
2
3
4
5
6
# 创建用户组        
group add elk
# 创建用户并添加至用户组
useradd elk -g elk
# (可选)更改用户密码
passwd elasticsearch

同时修改ES目录权限,以下操作都是为了赋予es用户操作权限

  • – 安装源码文件目录
1
2
3
[root@192-168-108-35 share]# chown -R elk:elk /usr/share/elasticsearch/
[root@192-168-108-35 share]# chown -R elk:elk /var/log/elasticsearch/
[root@192-168-108-35 share]# chown -R elk :elk /data

– 运行常见的报错信息

1
2
3
4
5
6
7
8
9
10
11
12
[1]文件数目不足# 修改系统配置文件属性 
# vim/etc/security/limits.conf 添加
elk soft memlock unlimited
elk hard memlock unlimited
elk soft nofile 65536
elk hard nofile 131072
退出用户重新登录,使配置生效
[2]: max virtual memoryareas vm.max_map_count [65530] is too low, increase to at least [262144]
解决办法:
在 /etc/sysctl.conf文件最后添加一行
vm.max_map_count=262144
即可永久修改

3. 启动elasticsearch服务

前台启动服务

1
2
3
4
# 需切换为es用户
su elk
# 启动服务(当前的路径为:/usr/share/elasticsearch/)
./bin/elasticsearch

后台运行ES

1
2
3
4
5
可以加入-p 命令 让es在后台运行, -p 参数 记录进程ID为一个文件
# 设置后台启动
./bin/elasticsearch -p ./elasticsearch-pid -d
一般情况下,直接执行一下命令即可
./bin/elasticsearch-d

结束进程

1
2
3
4
# 查看运行的pid,并查杀 
cat /tmp/elasticsearch-pid && echokill -SIGTERM {pid}
# 暴力结束进程(另一种方式)
kill -9 `ps -ef |grep elasticsearch|awk '{print $2}'

验证一下服务是否正常

1
curl -i "http://192.168.60.200:9200"

4.安装elasticsearch-head插件(支持前端界面查看数据) 安装docker镜像或者通过github下载elasticsearch-head项目都是可以的,1或者2两种方式选择一种安装使用即可

1
2
3
【1】使用docker的集成好的elasticsearch-head   
# dockerrun -p 9100:9100 mobz/elasticsearch-head:5
docker容器下载成功并启动以后,运行浏览器打开http://localhost:9100/

1
2
3
4
5
6
7
8
9
10
【2】使用git安装elasticsearch-head   
# yum install -y npm
# git clone git://github.com/mobz/elasticsearch-head.git
# cd elasticsearch-head
# npm install
# npm run start
检查端口是否起来
netstat -antp |grep 9100
浏览器访问测试是否正常
http://IP:9100/

【注意】由于elasticsearch-head:5镜像对elasticsearch的7版本好像适配性不够,所以部分显示可能会有空白。推荐另外一个镜像lmenezes/cerebro,下载后执行docker run -d -p 9000:9000lmenezes/cerebro就可以在9000端口查看了。

最后,filebeat logstash kibana可以在配置文件后,正常启动,如果需要切换用户的话,也可以参照上面。本次这三个组件全部默认用root用户启动。由于分开部署,与ES互不影响。

附:kafka三节点集群搭建

环境准备

1.zookeeper集群环境,本次采用Kafka自带的Zookeeper环境。
kafka是依赖于zookeeper注册中心的一款分布式消息对列,所以需要有zookeeper单机或者集群环境。

2.三台服务器:

1
2
3
192.168.108.200 ELK-kafka-cluster
192.168.108.165 ELK-kafka-salve1
192.168.108.103 ELK-kafka-salve2

3.下载kafka安装包

http://kafka.apache.org/downloads 中下载,目前最新版本的kafka已经到2.2.0,这里下载的是kafka_2.11-2.2.0.tgz

安装kafka集群

1.上传压缩包到三台服务器解压缩到/opt/目录下

1
tar -zxvf kafka_2.11-2.2.0.tgz -C /opt/ls -s kafka_2.11-2.2.0 kafka

2.修改server.properties (/opt/kafka/config目录下)

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
############################# Server Basics#############################
broker.id=0
######################## Socket Server Settings########################
listeners=PLAINTEXT://192.168.108.200:9092
advertised.listeners=PLAINTEXT://192.168.108.200:9092
num.network.threads=3
num.io.threads=8
socket.send.buffer.bytes=102400
socket.receive.buffer.bytes=102400
socket.request.max.bytes=104857600
############################# Log Basics#############################
log.dirs=/var/log/kafka-logs
num.partitions=1
num.recovery.threads.per.data.dir=1
######################## Internal Topic Settings#########################
offsets.topic.replication.factor=1
transaction.state.log.replication.factor=1
transaction.state.log.min.isr=1
######################### Log Retention Policy########################
# The minimum age of a log file to be eligiblefor deletion due to age
log.retention.hours=168
# The maximum size of a log segment file. Whenthis size is reached a new log segment will be created.
log.segment.bytes=1073741824
# The interval at which log segments are checkedto see if they can be deleted according
# to the retention policies
log.retention.check.interval.ms=300000
############################# Zookeeper#############################
zookeeper.connect=192.168.108.200:2181,192.168.108.165:2181,192.168.108.103:2181
# Timeout in ms for connecting to zookeeper
zookeeper.connection.timeout.ms=6000
delete.topic.enable=true
######################## Group CoordinatorSettings ##########################
group.initial.rebalance.delay.ms=0

3.拷贝两份到192.168.108.165和192.168.108.103

1
2
3
4
5
6
7
8
9
10
11
12
[root@192.168.108.165 config]# catserver.properties
broker.id=1
listeners=PLAINTEXT://192.168.108.165:9092
advertised.listeners=PLAINTEXT://192.168.108.165:9092
[root@k8s-n3 config]# cat server.properties
broker.id=2
listeners=PLAINTEXT://192.168.108.103:9092
advertised.listeners=PLAINTEXT://192.168.108.103:9092
然后添加环境变量 在/etc/profile 中添加
export ZOOKEEPER_HOME=/opt/kafka
export PATH=$PATH:$ZOOKEEPER_HOME/bin
source /etc/profile 重载生效

4.修改zookeeper.properties:

1.设置连接参数,添加如下配置

1
2
3
4
maxClientCnxns=100
tickTime=2000
initLimit=10
syncLimit=5

2.设置broker Id的服务地址

1
2
3
server.0=192.168.108.200:2888:3888
server.1=192.168.108.165:2888:3888
server.2=192.168.108.103:2888:3888

总结就在Kafka源码目录的/kafka/config/zookeeper.properties文件设置如下

1
2
3
4
5
6
7
8
9
10
11
dataDir=/opt/zookeeper         # 这时需要在/opt/zookeeper文件夹下,新建myid文件,把broker.id填写进去,本次本节点为0。
# the port at which the clients will connect
clientPort=2181
# disable the per-ip limit on the number of connections since this is a non-production config
maxClientCnxns=100
tickTime=2000
initLimit=10
syncLimit=5
server.0=192.168.108.200:2888:3888
server.1=192.168.108.165:2888:3888
server.2=192.168.108.103:2888:3888

然后在zookeeper数据目录添加id配置(dataDir=/opt/zookeeper)

1
2
3
4
5
6
7
8
9
在zookeeper.properties的数据目录中创建myid文件
在各台服务的zookeeper数据目录添加myid文件,写入服务broker.id属性值,如这里的目录是/usr/local/zookeeper/data
第一台broker.id为0的服务到该目录下执行:echo 0 > myid
[root@192-168-108-165 kafka]# cd /opt/zookeeper/
[root@192-168-108-165 zookeeper]# ls
myid version-2
[root@192-168-108-165 zookeeper]# cat myid
1
[root@192-168-108-165 zookeeper]#

集群启动

集群启动:cd /opt/kafka

先分别启动zookeeper

1
2
3
kafka使用到了zookeeper,因此你要首先启动一个zookeeper服务,如果你没有zookeeper服务。kafka中打包好了一个简洁版的单节点zookeeper实例。kafka启动时先启动zookeeper,再启动kafka;关闭时相反,先关闭kafka,再关闭zookeep
前台启动:bin/zookeeper-server-start.sh config/zookeeper.properties[root@192-168-108-200 ~]# lsof -i:2181COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODENAMEjava 5197 root 98u IPv6 527171 0t0 TCP *:eforward(LISTEN) 可以看到已经启动
后台启动:bin/zookeeper-server-start.sh-daemon config/zookeeper.properties

再分别启动kafka

1
2
3
4
5
6
7
8
9
10
前台启动:bin/kafka-server-start.sh  config/server.properties
[root@192-168-108-200 ~]# lsof -i:9092
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 5519 root 157u IPv6 528511 0t0 TCP 192-168-108-200:XmlIpcRegSvc (LISTEN)
java 5519 root 167u IPv6 528515 0t0 TCP 192-168-108-200:XmlIpcRegSvc->192.168.108.191:56656(ESTABLISHED)
java 5519 root 169u IPv6 528516 0t0 TCP192-168-108-200:XmlIpcRegSvc->192.168.108.187:49368 (ESTABLISHED)
java 5519 root 176u IPv6 528096 0t0 TCP192-168-108-200:48062->192-168-108-200:XmlIpcRegSvc (ESTABLISHED)
java 5519 root 177u IPv6 528518 0t0 TCP192-168-108-200:XmlIpcRegSvc->192-168-108-200:48062 (ESTABLISHED)
java 5519 root 178u IPv6 528097 0t0 TCP192-168-108-200:XmlIpcRegSvc->192.168.108.187:49370 (ESTABLISHED)
后台启动:bin/kafka-server-start.sh -daemon config/server.properties

服务启动脚本

1
2
3
4
cd /opt/kafka
kill -9 `ps -ef |grep kafka|awk '{print $2}' `
bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
bin/kafka-server-start.sh -daemonconfig/server.properties

Zookeeper+Kafka集群测试

/opt/kafka/bin

1.创建topic:

1
2
kafka-topics.sh --create--zookeeper 
192.168.108.200:2181,192.168.108.165:2181,192.168.108.103:2181--replication-factor 3 --partitions 3 --topic test

2.显示topic

1
2
3
4
5
6
kafka-topics.sh --describe--zookeeper 192.168.108.200:2181,192.168.108.165:2181,192.168.108.103:2181--topic test
[root@192-168-108-200 bin]# kafka-topics.sh--describe --zookeeper192.168.108.200:2181,192.168.108.165:2181,192.168.108.103:2181 --topic tyun
Topic:tyun PartitionCount:3 ReplicationFactor:3Configs:
Topic: tyun Partition: 0 Leader: 1 Replicas: 1,0,2Isr: 1,0,2
Topic: tyun Partition: 1 Leader: 2 Replicas: 2,1,0Isr: 2,1,0
Topic: tyun Partition: 2 Leader: 0 Replicas: 0,2,1Isr: 0,2,1
1
2
3
4
5
6
PartitionCount:partition个数   
ReplicationFactor:副本个数   
Partition:partition编号,从0开始递增   
Leader:当前partition起作用的broker.id   
Replicas: 当前副本数据所在的broker.id,是一个列表,排在最前面的其作用  
Isr:当前kakfa集群中可用的broker.id列表

3.列出topic

1
2
3
4
5
6
7
8
9
kafka-topics.sh --list --zookeeper192.168.108.200:2181,192.168.108.165:2181,192.168.108.103:2181
test
创建 producer(生产者);
kafka-console-producer.sh --broker-list192.168.108.200:9092 --topic test
hello

创建 consumer(消费者)
kafka-console-consumer.sh --bootstrap-server192.168.108.200:9092 --topic test --from-beginning
hello

4.查看写入kafka集群中的消息(重要命令,判断日志是否写入Kafka的重要依据)

1
bin/kafka-console-consumer.sh--bootstrap-server 192.168.108.200:9092 --topic tiops --from-beginning

5.删除 Topic—-命令标记删除后,再次删除对应的数据目录

1
2
3
4
5
6
7
bin/kafka-topics.sh--delete --zookeeper master:2181,slave1:2181,slave2:2181 --topic topic_name
若 delete.topic.enable=true
直接彻底删除该 Topic。
若delete.topic.enable=false
如果当前 Topic 没有使用过即没有传输过信息:可以彻底删除。
如果当前 Topic 有使用过即有过传输过信息:并没有真正删除 Topic 只是把这个 Topic 标记为删除(marked for deletion),重启 Kafka Server 后删除。
注:delete.topic.enable=true 配置信息位于配置文件 config/server.properties 中(较新的版本中无显式配置,默认为 true)。

4.日志系统架构解析

整个系统一共含有10台主机(filebeat部署在客户端,不计算在内),其中Logstash有四台,Elasticsearch有二台,Kafka集群三台,kibana一台并配置Nginx代理。

架构解释:

(1)首先用户通过nginx代理访问ELK日志统计平台,这里的Nginx可以设置界面密码。
(2)Nginx将请求转发到kibana
(3)kibana到Elasticsearch中去获取数据,这里的Elasticsearch是两台做的集群,日志数据会随机保存在任意一台Elasticsearch服务器。
(4)Logstash1从Kafka中取出数据并发送到Elasticsearch中。
(5)Kafka服务器做日志数据的持久化保存,避免web服务器日志量过大的时候造成的数据收集与保存不一致而导致日志丢失,其中Kafka可以做集群,然后再由Logstash服务器从Kafka持续的取出数据。
(6)logstash2从Filebeat取出的日志信息,并放入Kafka中进行保存。
(7)Filebeat在客户端进行日志的收集。

注1:【Kafka的加入原因与作用】

整个架构加入Kafka,是为了让整个系统更好的分层,Kafka作为一个消息流处理与持久化存储软件,能够帮助我们在主节点上屏蔽掉多个从节点之间不同日志文件的差异,负责管理日志端(从节点)的人可以专注于向 Kafka里生产数据,而负责数据分析聚合端的人则可以专注于从 Kafka内消费数据。所以部署时要把Kafka加进去。

而且使用Kafka进行日志传输的原因还在于其有数据缓存的能力,并且它的数据可重复消费,Kafka本身具有高可用性,能够很好的防止数据丢失,它的吞吐量相对来说比较好并且使用广泛。可以有效防止日志丢失和防止logsthash挂掉。综合来说:它均衡了网络传输,从而降低了网络闭塞,尤其是丢失数据的可能性,

注2:【双层的Logstash作用】

这里为什么要在Kafka前面增加二台logstash呢?是因为在大量的日志数据写入时,容易导致数据的丢失和混乱,为了解决这一问题,增加二台logstash可以通过类型进行汇总分类,降低数据传输的臃肿。

如果只有一层的Logstash,它将处理来自不同客户端Filebeat收集的日志信息汇总,并且进行处理分析,在一定程度上会造成在大规模日志数据下信息的处理混乱,并严重加深负载,所以有二层的结构进行负载均衡处理,并且职责分工,一层汇聚简单分流,一层分析过滤处理信息,并且内层都有二台Logstash来保障服务的高可用性,以此提升整个架构的稳定性。

接下来分别说明原理与各个组件之间的交互(配置文件)。

1.Filebeat与Logstash-collect连接配置

这里为了方便记忆,把此处的Logstash称为Logstash-collect,首先看Filebeat的配置文件:

其中,filebeat配置输出到logstash:5044端口,在logstash上启动5044端口作为logstash与filebeat的通信agent,而logstash本身服务起在9600端口.

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
[root@192-168-108-191 logstash]# ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 :::5044 :::*
LISTEN 0 50 ::ffff:192.168.108.191:9600 :::*

vim /etc/filebeat/filebeat.yml # 对几个重要配置进行设置如下
- type: log
# Change to true to enable this inputconfiguration.
enabled: true
# Paths that should be crawled and fetched.Glob based paths.
paths:
- /var/log/*.log
- /var/log/tiops/**/*.log # 即tiops平台的所有日志位置, 指定数据的输入路径为/tiops/**/*.log结尾的所有文件,注意/tiops/子目录下的日志不会被读取,孙子目录下的日志可以
#- c:programdataelasticsearchlogs*
fields:
service: filebeat
multiline: # 多行日志合并为一行,适用于日志中每一条日志占据多行的情况,比如各种语言的报错信息调用栈。
pattern: ‘^[‘
negate: true
match: after
# Excludelines. A list of regular expressions to match. It drops the lines that are
# 删除以DBG开头的行:
exclude_lines: ['^DBG']
# filebeat的日志可发送到logstash、elasticsearch、kibana,选择一个即可,本次默认选择logstash,其他二个可以注释
output.logstash:
# The Logstash hosts
hosts:["192.168.108.191:5044", "192.168.108.87:5044"] # 发往二台Logstash-collect
loadbalance: true
worker: 2
#output.elasticsearch:
#hosts: ["http://192.168.108.35:9200"]
#username: "elk"
#password: ""

#setup.kibana:
#host: "192.168.108.182:5601"

注1:【Filebeat与Logstash-collect的日志消息流转】

注意事项:Logstash-collect怎么接收到Filebeat发送的消息,并再次发向下一级呢?

不仅仅配置发往二台Logstash-collect的ip地址加端口,还要注意fields字段,可以理解为是一个Key,当Logstash-collect收到多个Key时,它可以选择其中一个或者多个Key来进行下一级发送,达到日志消息的转发。

在使用了6.2.3版本的ELK以后,如果使用if [type]配置,则匹配不到在filebeat里面使用document_type定义的字符串。因为6.0版本以上已经取消了document_type的定义。如果要实现以上的配置只能使用如下配置:

1
2
3
fields:
service: filebeat
service : filebeat都是自己定义的,定义完成后使用Logstash的if 判断,条件为if [fields][service] == "filebeat".就可以了,具体可以看下面的转发策略。

注2:【Filebeat负载平衡主机的输出】

这里还有一点需要说明一下,就是关于Filebeat与Logstash-collect连接的负载均衡设置。

logstash是一个无状态的流处理软件。logstash怎么集群配置只能横向扩展,然后自己用配置管理工具分发,因为他们内部并没有交流的。

Filebeat提供配置选项,可以使用它来调整负载平衡时发送消息到多个主机。要启用负载均衡,您指定loadbalance的值为true。

1
2
3
4
5
6
7
output.logstash:
# The Logstash hosts
hosts: ["192.168.108.191:5044","192.168.108.87:5044"] # 发往二台Logstash-collect
loadbalance:true
worker: 2
loadbalance: false # 消息只是往一个logstash里发,如果这个logstash挂了,就会自动将数据发到另一个logstash中。(主备模式)
loadbalance: true # 如果为true,则将数据均分到各个logstash中,挂了就不发了,往存活的logstash里面发送。

即logstash地址如果为一个列表,如果loadbalance开启,则负载到里表中的服务器,当一个logstash服务器不可达,事件将被分发到可到达的logstash服务器(双活模式)

loadbalance选项可供Redis,Logstash, Elasticsearch输出。Kafka的输出可以在其自身内部处理负载平衡。

同时每个主机负载平衡器还支持多个workers。默认是1。如果你增加workers的数量, 将使用额外的网络连接。workers参与负载平衡的总数=主机数量*workers。

在这个小节,配置总体结构,与日志数据流向如下图:

2.Logstash-collect与Kafka连接配置

首先看/etc/logstash/logstash.yml文件,作为Logstash-collect的公共配置文件,我们需要做一下的更改:

vim/etc/logstash/logstash.yml

  • path.data: /var/lib/logstash #数据存放路径
  • path.config: /etc/logstash/conf.d #配置文件的读取路径
  • path.logs: /var/log/logstash #日志文件的保存路径
  • pipeline.workers: 2 # 默认为CPU的核数
  • http.host: “192.168.108.186”
  • http.port: 9600-9700 # logstash will pick up thefirst available ports

注意上面配置文件的读取路径 /etc/logstash/conf.d,在这个文件夹里面,我们可以设置Logstash的输入输出。logstash支持把配置写入文件/etc/logstash/conf.d/xxx.conf,然后通过读取配置文件来采集数据。

logstash收集日志基本流程:input–>codec–>filter–>codec–>output ,所以配置文件可以设置输入输出与过滤的基本格式如下:(这里暂时忽略filter,因为这一层的Logstash主要汇聚数据,暂不分析匹配)

关于codec => json # json处理

logstash最终会把数据封装成json类型,默认会添加@timestamp时间字段、host主机字段、type字段。原消息数据会整个封装进message字段。如果数据处理过程中,用户解析添加了多个字段,则最终结果又会多出多个字段。也可以在数据处理过程中移除多个字段,总之,logstash最终输出的数据格式是json格式。所以数据经过Logstash会增加额外的字段,可以选择过滤。

Logstash主要由 input,filter,output三个组件去完成采集数据。

1
2
3
4
5
6
7
8
input {
指定输入
}
filter {
}
output {
指定输出
}

解释说明如下:

input
input组件负责读取数据,可以采用file插件读取本地文本文件,stdin插件读取标准输入数据,tcp插件读取网络数据,log4j插件读取log4j发送过来的数据等等。本次用beats插件读取Filebeat发送过来的日志消息。

filter
filter插件负责过滤解析input读取的数据,可以用grok插件正则解析数据,date插件解析日期,json插件解析json等等。

output
output插件负责将filter处理过的数据输出。可以用elasticsearch插件输出到es,redis插件输出到redis,stdout插件标准输出,kafka插件输出到kafka等等

在实际的Tiops平台日志处理流程中,配置如下:

二个Logstash-collect均采用此相同的配置

在/etc/logstash/conf.d目录下,创建filebeat-kafka.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
input {
beats {
port => 5044 # logstash上启动5044端口作为logstash与filebeat的通信agent
codec => json # json处理
}
}
#此处附加Redis作为缓存消息的配置,本次采用Kafka,所以此部分暂时注释
#output {
# if [fields][service] == "filebeat"{
# redis {
# data_type => "list"
# host => "192.168.108.200"
# db => "0"
# port => "6379"
# key => "tiops-tcmp"
# }
# }
#}
output {
if [fields][service] == "filebeat"{ # 参照第一部分,Logstash-collect怎么接收到Filebeat发送的消息,并再次发向下一级呢?这里就是取filebeat的Fields字段的Key值,进行向下发送
kafka {
bootstrap_servers =>"192.168.108.200:9092,192.168.108.165:9092,192.168.108.103:9092" # kafka集群配置,全部IP:端口都要加上
topic_id => "tiops" # 设置Kafka的topic,这样发送到kafka中时,kafka就会自动创建该topic
}
}
}

在这个小节,配置总体结构与日志数据流向如下图:

3.Kafka与Logstash-grok连接配置

在上面第二部分,我们已经成功把数据从Logstash-collect发送到Kafka,而Kafka本身的输入输出并没有配置。而是Logstash-collect指定了一个topic,这并不代表kafka不需要配置参数,只是kafka本身不需要配置pull/push目标参数,它被动接受

别的生产者发送过来的数据,因为Logstash-collect指定了Kafka集群的IP地址和端口。那么数据将会被发送到Kafka中,那么Kafka如何处理这些日志数据,那么这时候就需要Kafka配置消息处理参数了。

这个我们现在暂时先给出参数,至于具体原因涉及到MQ的高可用、消息过期策略、如何保证消息不重复消费、如何保证消息不丢失、如何保证消息按顺序执行以及消息积压在消息队列里怎么办等问题,我们之后详细分析并解决。

以下是Kafka集群中,某一台的配置文件,在Kafka源码目录的/kafka/config/server.properties文件设置如下:(加粗的为重要配置文件)

  • broker.id=0
  • listeners=PLAINTEXT://192.168.108.200:9092
  • advertised.listeners=PLAINTEXT://192.168.108.200:9092
  • num.network.threads=3
  • num.io.threads=8
  • socket.send.buffer.bytes=102400
  • socket.receive.buffer.bytes=102400
  • socket.request.max.bytes=104857600
  • log.dirs=/var/log/kafka-logs
  • num.partitions=1
  • num.recovery.threads.per.data.dir=1
  • offsets.topic.replication.factor=1
  • transaction.state.log.replication.factor=1
  • transaction.state.log.min.isr=1
  • log.retention.hours=168
  • log.segment.bytes=1073741824
  • log.retention.check.interval.ms=300000
  • zookeeper.connect=192.168.108.200:2181,192.168.108.165:2181,192.168.108.103:2181 # zookeeper 集群
  • delete.topic.enable=true

同时kafka是依赖于zookeeper注册中心的一款分布式消息队列,所以需要有zookeeper单机或者集群环境。本次采用Kafka自带的Zookeeper环境。

在Kafka源码目录的/kafka/config/zookeeper.properties文件设置如下:

  • dataDir=/opt/zookeeper # 这时需要在/opt/zookeeper文件夹下,新建myid文件,把broker.id填写进去,本次本节点为0。
  • # the port at which the clients will connect
  • clientPort=2181
  • # disable the per-ip limit on the number of connections since this is a non-production config
  • maxClientCnxns=100
  • tickTime=2000
  • initLimit=10
  • syncLimit=5
  • server.0=192.168.108.200:2888:3888
  • server.1=192.168.108.165:2888:3888
  • server.2=192.168.108.103:2888:3888

其他注意事项,请看之前的Kafka集群安装步骤,这里主要给出集群某一节点的配置文件参数。

到目前为止,我们配置了kafka的参数,对消息进行了处理(持久化…),接下来就需要对Kafka中的消息进行下一步的传输,即传送到Logstash-grok。

这里需要注意的是:并不是Kafka主动把消息发送出去的,当然Kafka也支持这种操作,但是在这里,采用的是Logstash-grok作为消费者,主动拉取Kafka中的消息,来进行消费。

所以就需要像第二步Logstash-collect那样进行输入输出设置,这里Logstash-grok不仅仅配置输入输出,最重要的是它的过滤filter作用。

在/etc/logstash/conf.d目录下,创建logstash-es.conf文件,这里给出Logstash-grok中一个的内容如下(二个Logstash-grok配置一样):

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
#input {
# redis {
# data_type => "list"
# host => "192.168.108.200"
# db => "0"
# port => "6379"
# key => "tiops-tcmp"
# # password => "123456"
# }
#}
input{
kafka{
bootstrap_servers =>["192.168.108.200:9092,192.168.108.165:9092,192.168.108.103:9092"] #配置拉取的kafka集群消息地址
group_id => "tyun" # 设置group_id,对于这个消费组而言会有一个消费 offset,消费掉是 auto_commit 部分控制的,这个参数会定期将你消费的 offset 提交到 kafka,下次启动的时候已经消费过的就不会重复消费了;
# 如果想重新消费,可以换一个 group_id 即可
auto_offset_reset =>"earliest"
consumer_threads => "3" #多个实例的consumer_threads数之和应该等于topic分区数近似达到logstash多实例的效果。
decorate_events => "false"
topics => ["tiops"] # kafka topic 名称,获取指定topic内的消息
type => "log"
codec => json
}
}
filter { # 此处Logstash最主要的过滤作用,可以自定义正则表达式,选择性输出消息
grok {
match => {
#截取
"message" =>"(?<LOG>(?<=architecture=x86_64}|containerized=true})(.*)/?)"
}
}
}
output { #过滤后的消息,发送到ES集群
elasticsearch {
hosts =>["192.168.108.35:9200","192.168.108.194:9200"]
codec => json
index =>"tiops-%{+YYYY.MM.dd}" # 自定义索引名称
}

在这个小节,配置总体结构与日志数据流向如下图:

4.Logstash-grok与Elasticsearch(ES)集群连接配置

ElasticSearch集群
在第三步中,我们已经将Logstash-grok过滤后的消息,发送到了ES集群,这里的ES集群被动接受发送过来的消息,即ES集群本身不需要配置输入源,已经由其他组件发送决定了。

ES的配置将集中体现在集群自身的配置上。

Elasticsearch 可以横向扩展至数百(甚至数千)的服务器节点,同时可以处理PB级数据
Elasticsearch 天生就是分布式的,并且在设计时屏蔽了分布式的复杂性。

ES功能:(1)分布式的搜索引擎和数据分析引擎

​ (2)全文检索,结构化检索,数据分析

​ (3)对海量数据进行近实时的处理

安装后的ES集群,配置在/etc/elasticsearch/elasticsearch.yml文件,集群中,各个节点配置大体相同,不同之处下面有红字标出,主要就是节点名称与网络监听地址,需要每个节点自行按实际填写。

  • cluster.name: elk-cluster #集群名称
  • node.name: elk-node1 #节点名称,一个集群之内节点的名称不能重复
  • path.data: /data/elkdata #数据路径
  • path.logs: /data/elklogs #日志路径
  • # bootstrap.memory_lock: true #锁住es内存,保证内存不分配至交换分区。
  • network.host: 192.168.108.35 #网络监听地址,可以访问elasticsearch的ip, 默认只有本机
  • http.port: 9200 #用户访问查看的端口,9300是ES组件访问使用
  • discovery.seed_hosts: [“192.168.108.35”,”192.168.108.194”] 单播(配置一台即可,生产可以使用组播方式)
  • cluster.initial_master_nodes: [“192.168.108.35”] # Master
  • http.cors.enabled: true # 这二个是ES插件的web显示设置,head插件防止跨域
  • http.cors.allow-origin: “*”

可见集群配置中最重要的两项是node.name与network.host,每个节点都必须不同。其中node.name是节点名称主要是在Elasticsearch自己的日志加以区分每一个节点信息。

discovery.seed_hosts是集群中的节点信息,可以使用IP地址、可以使用主机名(必须可以解析)。

当ElasticSearch的节点启动后,它会利用多播(multicast)(或者单播),如果用户更改了配置)寻找集群中的其它节点,并与之建立连接。(节点发现)

这里没有配置分片的数目,在ES的7版本中,默认为1,这个包括replicas可后来自定义设置。

在这个小节,配置总体结构与日志数据流向如下图:

5.Elasticsearch(ES)集群与Kibana连接配置

Kibana是一个开源的分析和可视化平台,和Elasticsearch一起工作时,就可以用Kibana来搜索,查看,并和存储在Elasticsearch索引中的数据进行交互。

可以轻松地执行高级数据分析,并且以各种图标、表格和地图的形式可视化数据。

同时Kibana使得理解大量数据变得很容易。它简单的、基于浏览器的界面使你能够快速创建和共享动态仪表板,实时显示Elasticsearch查询的变化。

在第四步中,我们已经将ES集群中的数据进行了处理,建立了索引(相当于MySQL的database概念),并且分片、副本等都存在,而且处理后的数据更加方便于检索。

Kibana的配置在/etc/kibana/kibana.yml文件中,如下:

  • server.port: 5601 #监听端口
  • server.host: “192.168.108.182” #监听IP地址,建议内网ip
  • #kibana.index: “.newkibana”
  • elasticsearch.hosts: [“http://192.168.108.35:9200","http://192.168.108.194:9200"] # ES机器的IP,这里的所有host必须来自于同一个ES集群
  • #xpack.security.enabled: false #添加这条,这条是配置kibana的安全机制,暂时关闭。

可以看出,这里的Kibana会主动去ES集群中去拉取数据,最后在浏览器Web端进行实时展示。

这时,我们打开浏览器界面如下,看看最终的结果:

注:Nginx代理配置,可以通过自定义域名访问并设置密码,提升安全性。(可选)

1
2
3
4
5
6
7
8
9
10
11
upstream kibana_server {
server 192.168.108.182:5601weight=1 max_fails=3 fail_timeout=60;
}
server {
listen 80;
server_name www.kibana.com;
auth_basic "RestrictedAccess";
auth_basic_user_file/etc/nginx/conf.d/htpasswd.users;
location / {
proxy_pass http://kibana_server;
proxy_http_version 1.1;

在这个小节,配置总体结构与日志数据流向如下图:

6.总结

再将日志流向的配置文件输入输出标注一下:

转载自 : https://wsgzao.github.io/post/elk/

前言

Elasticsearch + Logstash + Kibana(ELK)是一套开源的日志管理方案,分析网站的访问情况时我们一般会借助 Google / 百度 / CNZZ 等方式嵌入 JS 做数据统计,但是当网站访问异常或者被攻击时我们需要在后台分析如 Nginx 的具体日志,而 Nginx 日志分割 / GoAccess/Awstats 都是相对简单的单节点解决方案,针对分布式集群或者数据量级较大时会显得心有余而力不足,而 ELK 的出现可以使我们从容面对新的挑战。

  • Logstash:负责日志的收集,处理和储存
  • Elasticsearch:负责日志检索和分析
  • Kibana:负责日志的可视化

ELK(Elasticsearch + Logstash + Kibana)


扩展阅读

elastic - https://www.elastic.co/cn/
ELK - https://fainyang.github.io/post/elk/


ELK 简介

ELK 官方文档 是一个分布式、可扩展、实时的搜索与数据分析引擎。目前我在工作中只用来收集 server 的 log, 开发锅锅们 debug 的好助手。

安装设置单节点 ELK

如果你想快速的搭建单节点 ELK, 那么使用 docker 方式肯定是你的最佳选择。使用三合一的镜像,文档详情
注意:安装完 docker, 记得设置 mmap counts 大小至少 262144
什么是 mmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 设置 mmap 命令
# 临时添加法
sysctl -w vm.max_map_count=262144

# 写入 sysctl.conf 文件里
vim /etc/sysctl.conf

vm.max_map_count=262144
# 保存好文件执行以下命令

sysctl -p

# 安装 docker
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 -y docker-ce

sudo systemctl start docker

单节点的机器,不必暴露 9200(Elasticsearch JSON interface) 和 9300(Elasticsearch transport interface) 端口。
如果想在 docker 上暴露端口,用 -p 如果没有填写监听的地址,默认是 0.0.0.0 所有的网卡。建议还是写明确监听的地址,安全性更好。

1
2
-p 监听的 IP: 宿主机端口: 容器内的端口
-p 192.168.10.10:9300:9300

命令行启动一个 ELK

1
2
3
4
5
sudo docker run -p 5601:5601 -p 5044:5044 \
-v /data/elk-data:/var/lib/elasticsearch \
-v /data/elk/logstash:/etc/logstash/conf.d \
-it -e TZ="Asia/Singapore" -e ES_HEAP_SIZE="20g" \
-e LS_HEAP_SIZE="10g" --name elk-ubuntu sebp/elk

将配置和数据挂载出来,即使 docker container 出现了问题。可以立即销毁再重启一个,服务受影响的时间很短。

1
2
3
4
5
6
# 注意挂载出来的文件夹的权限问题
chmod 755 /data/elk-data
chmod 755 /data/elk/logstash
chown -R root:root /data
-v /data/elk-data:/var/lib/elasticsearch # 将 elasticsearch 存储的数据挂载出来,数据持久化。
-v /data/elk/logstash:/etc/logstash/conf.d # 将 logstash 的配置文件挂载出来,方便在宿主机上修改。

elasticsearch 重要的参数调优

  1. ES_HEAP_SIZE Elasticsearch will assign the entire heap specified in jvm.options via the Xms (minimum heap size) and Xmx (maximum heap size) settings. You should set these two settings to be equal to each other. Set Xmx and Xms to no more than 50% of your physical RAM.the exact threshold varies but is near 32 GB. the exact threshold varies but 26 GB is safe on most systems, but can be as large as 30 GB on some systems.
    利弊关系: The more heap available to Elasticsearch, the more memory it can use for its internal caches, but the less memory it leaves available for the operating system to use for the filesystem cache. Also, larger heaps can cause longer garbage collection pauses.
  2. LS_HEAP_SIZE 如果 heap size 过低,会导致 CPU 利用率到达瓶颈,造成 JVM 不断的回收垃圾。 不能设置 heap size 超过物理内存。 至少留 1G 给操作系统和其他的进程。

只需要配置 logstash

接下来,我们再来看一看 logstash.conf 记得看注释
参考链接:

  1. SSL 详情可参考
  2. grok 正则捕获
  3. grok 插件语法介绍
  4. logstash 配置语法
  5. grok 内置 pattern
  6. 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
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
input {
beats {
port => 5044
#ssl => true
#ssl_certificate => "/etc/logstash/logstash.crt"
#ssl_key => "/etc/logstash/logstash.key"
# 1. SSL 详情可参考
}
}
# filter 模块主要是数据预处理,提取一些信息,方便 elasticsearch 好归类存储。
# 2. grok 正则捕获
# 3. grok 插件语法介绍
# 4. logstash 配置语法
# 5. grok 内置 pattern
filter {
grok {
match => {"message" => "%{EXIM_DATE:timestamp}\|%{LOGLEVEL:log_level}\|%{INT:pid}\|%{GREEDYDATA}"}
# message 字段是 log 的内容,例如 2018-12-11 23:46:47.051|DEBUG|3491|helper.py:85|helper._save_to_cache|shop_session
# 在这里我们提取出了 timestamp log_level pid,grok 有内置定义好的 patterns: EXIM_DATE, EXIM_DATE, INT
# GREEDYDATA 贪婪数据,代表任意字符都可以匹配
}
# 我们在 filebeat 里面添加了这个字段[fields][function] 的话,那就会执行对应的 match 规则去匹配 path
# source 字段就是 log 的来源路径,例如 /var/log/nginx/feiyang233.club.access.log
# match 后我们就可以得到 path=feiyang233.club.access
if [fields][function]=="nginx" {
grok {
match => {"source" => "/var/log/nginx/%{GREEDYDATA:path}.log%{GREEDYDATA}"}
}
}
# 例如 ims 日志来源是 /var/log/ims_logic/debug.log
# match 后我们就可以得到 path=ims_logic
else if [fields][function]=="ims" {
grok {
match => {"source" => "/var/log/%{GREEDYDATA:path}/%{GREEDYDATA}"}
}
}

else {
grok {
match => {"source" => "/var/log/app/%{GREEDYDATA:path}/%{GREEDYDATA}"}
}
}
# filebeat 有定义 [fields][function] 时,我们就添加上这个字段,例如 QA
if [fields][function] {
mutate {
add_field => {
"function" => "%{[fields][function]}"
}
}
}
# 因为线上的机器更多,线上的我默认不在 filebeat 添加 function,所以 else 我就添加上 live
else {
mutate {
add_field => {
"function" => "live"
}
}
}
# 在之前 filter message 时,我们得到了 timestamp,这里我们修改一下格式,添加上时区。
date {
match => ["timestamp" , "yyyy-MM-dd HH:mm:ss Z"]
target => "@timestamp"
timezone => "Asia/Singapore"
}
# 将之前获得的 path 替换其中的 / 替换为 - , 因为 elasticsearch index name 有要求
# 例如 feiyang/test feiyang_test
mutate {
gsub => ["path","/","-"]
add_field => {"host_ip" => "%{[fields][host]}"}
remove_field => ["tags","@version","offset","beat","fields","exim_year","exim_month","exim_day","exim_time","timestamp"]
}
# remove_field 去掉一些多余的字段
}
# 单节点 output 就在本机,也不需要 SSL, 但 index 的命名规则还是需要非常的注意
output {
elasticsearch {
hosts => ["localhost:9200"]
index => "sg-%{function}-%{path}-%{+xxxx.ww}"
# sg-nginx-feiyang233.club.access-2019.13 ww 代表周数
}
}

最终的流程图如下所示


index 的规则 参考链接

  • Lowercase only
  • Cannot include \, /, *, ?, “, <, >, |, (space character), ,, #
  • Indices prior to 7.0 could contain a colon (:), but that’s been deprecated and won’t be supported in 7.0+
  • Cannot start with -, _, +
  • Cannot be . or ..
  • Cannot be longer than 255 bytes (note it is bytes, so multi-byte characters will count towards the 255 limit faster)

filebeat 配置

在 client 端,我们需要安装并且配置 filebeat 请参考
Filebeat 模块与配置
配置文件 filebeat.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
filebeat.inputs:
- type: log
enabled: true
paths: # 需要收集的日志
- /var/log/app/** ## ** need high versiob filebeat can support recursive

fields: # 需要添加的字段
host: "{{inventory_hostname}}"
function: "xxx"
multiline: # 多行匹配
match: after
negate: true # pay attention the format
pattern: '^\[[0-9]{4}-[0-9]{2}-[0-9]{2}' #\[
ignore_older: 24h
clean_inactive: 72h

output.logstash:
hosts: ["{{elk_server}}:25044"]
# ssl:
# certificate_authorities: ["/etc/filebeat/logstash.crt"]

批量部署 filebeat.yml 最好使用 ansible

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
---
- hosts: all
become: yes
gather_facts: yes
tasks:
- name: stop filebeat
service:
name: filebeat
state: stopped
enabled: yes

- name: upload filebeat.yml
template:
src: filebeat.yml
dest: /etc/filebeat/filebeat.yml
owner: root
group: root
mode: 0644

- name: remove
file: #delete all files in this directory
path: /var/lib/filebeat/registry
state: absent

- name: restart filebeat
service:
name: filebeat
state: restarted
enabled: yes

查看 filebeat output

首先需要修改配置,将 filebeat 输出到本地的文件,输出的格式为 json.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/app/**
fields:
host: "x.x.x.x"
region: "sg"
multiline:
match: after
negate: true
pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}'
ignore_older: 24h
clean_inactive: 72h

output.file:
path: "/home/feiyang"
filename: feiyang.json

通过上述的配置,我们就可以在路径 /home/feiyang 下得到输出结果文件 feiyang.json 在这里需要注意的是,不同版本的 filebeat 输出结果的格式会有所不同,这会给 logstash 解析过滤造成一点点困难。下面举例说明 6.x 和 7.x filebeat 输出结果的不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"@timestamp": "2019-06-27T15:53:27.682Z",
"@metadata": {
"beat": "filebeat",
"type": "doc",
"version": "6.4.2"
},
"fields": {
"host": "x.x.x.x",
"region": "sg"
},
"host": {
"name": "x.x.x.x"
},
"beat": {
"name": "x.x.x.x",
"hostname": "feiyang-localhost",
"version": "6.4.2"
},
"offset": 1567983499,
"message": "[2019-06-27T22:53:25.756327232][Info][@http.go.177] [48552188]request",
"source": "/var/log/feiyang/scripts/all.log"
}

6.4 与 7.2 还是有很大的差异,在结构上。

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
{
"@timestamp": "2019-06-27T15:41:42.991Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "7.2.0"
},
"agent": {
"id": "3a38567b-e6c3-4b5a-a420-f0dee3a3bec8",
"version": "7.2.0",
"type": "filebeat",
"ephemeral_id": "b7e3c0b7-b460-4e43-a9af-6d36c25eece7",
"hostname": "feiyang-localhost"
},
"log": {
"offset": 69132192,
"file": {
"path": "/var/log/app/feiyang/scripts/info.log"
}
},
"message": "2019-06-27 22:41:25.312|WARNING|14186|Option|data|unrecognized|fields=set([u'id'])",
"input": {
"type": "log"
},
"fields": {
"region": "sg",
"host": "x.x.x.x"
},
"ecs": {
"version": "1.0.0"
},
"host": {
"name": "feiyang-localhost"
}
}

Kibana 简单的使用

在搭建 ELK 时,暴露出来的 5601 端口就是 Kibana 的服务。
访问 http://your_elk_ip:5601

安装设置集群 ELK 版本 6.7

ELK 安装文档集群主要是高可用,多节点的 Elasticsearch 还可以扩容。本文中用的官方镜像 The base image is centos:7

Elasticsearch 多节点搭建

官方安装文档 Elasticsearch

1
2
3
4
5
6
7
# 挂载出来的文件夹权限非常的重要
mkdir -p /data/elk-data && chmod 755 /data/elk-data
chown -R root:root /data
docker run -p WAN_IP:9200:9200 -p 10.66.236.116:9300:9300 \
-v /data/elk-data:/usr/share/elasticsearch/data \
--name feiy_elk \
docker.elastic.co/elasticsearch/elasticsearch:6.7.0

接下来是修改配置文件 elasticsearch.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Master 节点 node-1
# 进入容器 docker exec -it [container_id] bash
# docker exec -it 70ada825aae1 bash
# vi /usr/share/elasticsearch/config/elasticsearch.yml
cluster.name: "feiy_elk"
network.host: 0.0.0.0
node.master: true
node.data: true
node.name: node-1
network.publish_host: 10.66.236.116
discovery.zen.ping.unicast.hosts: ["10.66.236.116:9300","10.66.236.118:9300","10.66.236.115:9300"]

# exit
# docker restart 70ada825aae1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# slave 节点 node-2
# 进入容器 docker exec -it [container_id] bash
# vi /usr/share/elasticsearch/config/elasticsearch.yml
cluster.name: "feiy_elk"
network.host: "0.0.0.0"
node.name: node-2
node.data: true
network.publish_host: 10.66.236.118
discovery.zen.ping.unicast.hosts: ["10.66.236.116:9300","10.66.236.118:9300","10.66.236.115:9300"]

# exit
# docker restart 70ada825aae1
# slave 节点 node-3
# 进入容器 docker exec -it [container_id] bash
# vi /usr/share/elasticsearch/config/elasticsearch.yml
cluster.name: "feiy_elk"
network.host: "0.0.0.0"
node.name: node-3
node.data: true
network.publish_host: 10.66.236.115
discovery.zen.ping.unicast.hosts: ["10.66.236.116:9300","10.66.236.118:9300","10.66.236.115:9300"]

# exit
# docker restart 70ada825aae1

检查集群节点个数,状态等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# curl http://wan_ip:9200/_cluster/health?pretty
{
"cluster_name" : "feiy_elk",
"status" : "green",
"timed_out" : false,
"number_of_nodes" : 3,
"number_of_data_nodes" : 3,
"active_primary_shards" : 9,
"active_shards" : 18,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 0,
"delayed_unassigned_shards" : 0,
"number_of_pending_tasks" : 0,
"number_of_in_flight_fetch" : 0,
"task_max_waiting_in_queue_millis" : 0,
"active_shards_percent_as_number" : 100.0
}

最终结果图在 kibana 上可以看到集群状态

Kibana 搭建

官方安装文档 Kibana

1
2
3
4
5
6
# docker run --link YOUR_ELASTICSEARCH_CONTAINER_NAME_OR_ID:elasticsearch -p 5601:5601 {docker-repo}:{version}
docker run -p 外网 IP:5601:5601 --link elasticsearch 容器的 ID:elasticsearch docker.elastic.co/kibana/kibana:6.7.0

# 注意的是 --link 官方其实并不推荐的,推荐的是 use user-defined networks https://docs.docker.com/network/links/
# 测试不用 --link 也可以通。直接用容器的 IP
docker run -p 外网 IP:5601:5601 docker.elastic.co/kibana/kibana:6.7.0

we recommend that you use user-defined networks to facilitate communication between two containers instead of using –link

1
2
3
4
5
6
7
8
9
10
11
# vi /usr/share/kibana/config/kibana.yml
# 需要把 hosts IP 改为 elasticsearch 容器的 IP
# 我这里 elasticsearch 容器的 IP 是 172.17.0.2
# 如何查看 docker inspect elasticsearch_ID
server.name: kibana
server.host: "0.0.0.0"
elasticsearch.hosts: [ "http://172.17.0.2:9200" ]
xpack.monitoring.ui.container.elasticsearch.enabled: true

# 退出容器并重启
docker restart [container_ID]

Logstash 搭建

官方安装文档 Logstash

1
2
3
4
# docker -d 以后台的方式启动容器  --name 参数显式地为容器命名
docker run -p 5044:5044 -d --name test_logstash docker.elastic.co/logstash/logstash:6.7.0
# 也可以指定网卡,监听在内网或者外网 监听在内网 192.168.1.2
docker run -p 192.168.1.2:5044:5044 -d --name test_logstash docker.elastic.co/logstash/logstash:6.7.0
1
2
3
4
# vi /usr/share/logstash/pipeline/logstash.conf
# 配置详情请参考下面的链接, 记得 output hosts IP 指向 Elasticsearch 的 IP
# Elasticsearch 的默认端口是 9200,在下面的配置中可以省略。
hosts => ["IP Address 1:port1", "IP Address 2:port2", "IP Address 3"]

logstash 过滤规则 见上文的配置和 grok 语法规则

1
2
3
4
5
6
# vi /usr/share/logstash/config/logstash.yml
# 需要把 url 改为 elasticsearch master 节点的 IP
http.host: "0.0.0.0"
xpack.monitoring.elasticsearch.url: http://elasticsearch_master_IP:9200
node.name: "feiy"
pipeline.workers: 24 # same with cores

改完配置 exit 从容器里退出到宿主机,然后重启这个容器。更多配置详情,参见官方文档

1
2
3
4
# 如何查看 container_ID
docker ps -a

docker restart [container_ID]

容灾测试

我们把当前的 master 节点 node-1 关机,通过 kibana 看看集群的状态是怎样变化的。

当前集群的状态变成了黄色,因为还有 3 个 Unassigned Shards。颜色含义请参考官方文档,再过一会发现集群状态变成了绿色。

kibana 控制台 Console

Quick intro to the UI
The Console UI is split into two panes: an editor pane (left) and a response pane (right). Use the editor to type requests and submit them to Elasticsearch. The results will be displayed in the response pane on the right side.

Console understands requests in a compact format, similar to cURL:

1
2
3
4
5
6
7
8
# index a doc
PUT index/type/1
{
"body": "here"
}

# and get it ...
GET index/type/1

While typing a request, Console will make suggestions which you can then accept by hitting Enter/Tab. These suggestions are made based on the request structure as well as your indices and types.

A few quick tips, while I have your attention

  • Submit requests to ES using the green triangle button.
  • Use the wrench menu for other useful things.
  • You can paste requests in cURL format and they will be translated to the Console syntax.
  • You can resize the editor and output panes by dragging the separator between them.
  • Study the keyboard shortcuts under the Help button. Good stuff in there!

Console 常用的命令

Kibana 控制台
ELK 技术栈中的那些查询语法

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
GET _search
{
"query": {
"match_all": {}
}
}

GET /_cat/health?v

GET /_cat/nodes?v

GET /_cluster/allocation/explain

GET /_cluster/state

GET /_cat/thread_pool?v

GET /_cat/indices?health=red&v

GET /_cat/indices?v

# 将当前所有的 index 的 replicas 设置为 0

PUT /*/_settings
{
"index" : {
"number_of_replicas" : 0,
"refresh_interval": "30s"
}
}

GET /_template


# 在单节点的时候,不需要备份,所以将 replicas 设置为 0
PUT _template/app-logstash
{
"index_patterns": ["app-*"],
"settings": {
"number_of_shards": 3,
"number_of_replicas": 0,
"refresh_interval": "30s"
}
}

Elasticsearch 数据迁移

Elasticsearch 数据迁移官方文档感觉不是很详细。容器化的数据迁移,我太菜用 reindex 失败了,snapshot 也凉凉。
最后是用一个开源工具 An Elasticsearch Migration Tool 进行数据迁移的。

1
2
3
wget https://github.com/medcl/esm-abandoned/releases/download/v0.4.2/linux64.tar.gz
tar -xzvf linux64.tar.gz
./esm -s http://127.0.0.1:9200 -d http://192.168.21.55:9200 -x index_name -w=5 -b=10 -c 10000 --copy_settings --copy_mappings --force --refresh

Nginx 代理转发

因为有时候 docker 重启,iptables restart 也会刷新,所以导致了我们的限制规则会被更改,出现安全问题。这是由于 docker 的网络隔离基于 iptable 实现造成的问题。为了避免这个安全问题,我们可以在启动 docker 时,就只监听在内网,或者本地 127.0.0.1 然后通过 nginx 转发。

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
# cat kibana.conf
server {

listen 25601;
server_name x.x.x.x;
access_log /var/log/nginx/kibana.access.log;
error_log /var/log/nginx/kibana.error.log;

location / {
allow x.x.x.x;
allow x.x.x.x;
deny all;

proxy_http_version 1.1;
proxy_buffer_size 64k;
proxy_buffers 32 32k;
proxy_busy_buffers_size 128k;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_pass http://127.0.0.1:5601;

}
}

! 这里需要注意的是, iptable filter 表 INPUT 链 有没有阻挡 172.17.0.0/16 docker 默认的网段。是否阻挡了 25601 这个端口。

踩过的坑

  • iptables 防不住。需要看 上一篇博客 里的 iptable 问题。或者监听在内网,用 Nginx 代理转发。

  • elk 网络问题

  • elk node

  • discovery.type=single-node 在测试单点时可用,搭建集群时不能设置这个环境变量,详情见官方文档

  • ELK 的一次吞吐量优化

  • filebeat 版本过低导致 recursive glob patterns ** 不可用
    用 ansible 升级 filebeat

    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
    ---
    - hosts: all
    become: yes
    gather_facts: yes
    tasks:
    - name: upload filebeat.repo
    copy:
    src: elasticsearch.repo
    dest: /etc/yum.repos.d/elasticsearch.repo
    owner: root
    group: root
    mode: 0644

    - name: install the latest version of filebeat
    yum:
    name: filebeat
    state: latest

    - name: restart filebeat
    service:
    name: filebeat
    state: restarted
    enabled: yes

    # elasticsearch.repo
    [elasticsearch-6.x]
    name=Elasticsearch repository for 6.x packages
    baseurl=https://artifacts.elastic.co/packages/6.x/yum
    gpgcheck=1
    gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
    enabled=1
    autorefresh=1
    type=rpm-md
  • filebeat 7.x 与 6.x 不兼容问题. 关键字变化很大, 比如说 “sorce” 变为了 [log][file][path]

参考文章

  1. 腾讯云 Elasticsearch Service 这个腾讯云的专栏非常的不错,请您一定要点开看一眼,总有你想要的。
  2. ELK 重难点总结和整体优化配置

前言

整理了 Golang 面试用的笔记,仅供参考


扩展阅读


##

本文结构:

1
2
3
4
5
6
7
8
└──计算机基础
├── 计算机网络
├── 数据结构
├── 算法
├── 操作系统
├── 数据库
└── OOP 与设计模式
└── Golang 面试题

参考资料:笔试面试知识整理Golang 面试题解析Go面试题答案与解析

这篇文章的内容力大多一笔带过,细节上我参考的书籍有:

计算机网络

img

  • TCP / UDP 传输层:端到端的服务
  • IP 网络层:点到点的服务

HTTP 协议

请求报文

1
2
3
<method> <request-URL> <version>	# 状态行		# 状态行
<headers> # 请求头 # 响应头
<entity-body> # 请求实体 # 响应体

HTTP 协议不限制 GET URL 长度,但浏览器限制字符数(Chrome 8K)& 也不限制 POST 资源大小

  • GET:查
    • 安全的:获取信息,非修改信息。一般不会产生副作用
    • 幂等的:同一 URL 多个请求返回同一结果
  • POST:改,服务端通过请求 header 的 Content-Type 字段解析实体数据。提交数据的方式:
    • application/x-www-form-urlencoded:浏览器原生的 form 表单,提交的数据按 url 编码,Ajax大多默认使用
    • form_data:表单文件上传
    • application/json:API 使用较多

响应报文

状态行:协议版本、状态码、状态描述

1: Informational

  • 100 continue:POST 提交数据大于 100KB 时候发的第一个请求,允许上传则返回 100

2:Success

  • 200 OK:请求被成功处理,GET 返回资源、POST 返回对请求处理的结果
  • 204 No Content:请求处理完毕,但不返回响应体,客户端页面不刷新

3:Redirection

  • 301 Moved Permanently:永久重定向,资源分配了新的 URI,处理:

    • HEAD:响应头的 Location 指明新的 URI
    • GET:Location 指明新 URI & 在响应体中附上 URI
  • 302 Moved Temporarily:临时性重定向,希望用户本次能访问新的 URI。重定向后的请求方法不变

  • 303 See Other:请求资源有另一个 URI,重定向后的方法变为 GET 获取新 URI

    注意: 很多浏览器将 303 理解为 302,直接使用 GET 请求 Location 中的 URI

  • 304 Not Modified:请求头中带有 If-ModifiedIf-Match,自从上次请求后资源并未更新,则不发送响应体。

4:Client Error

  • 400 Bad Request:请求报文有语法错误

  • 401 Unauthorized:请求需要有 HTTP 认证(nginx 的 auth_basic),返回 401 时头部中

    www-authenticate 指明认证方式,再次请求时也需带上 authorization 认证信息

  • 403 Forbidden:访问被拒绝,响应实体中可说明原因

  • 404 Not Found:请求资源不存在、403 不想说明原因

5:Server Error

  • 500 Internel Error:服务器处理请求出错
  • 502 Bad Gateway: 服务器作为代理,从 upstream 收到无效响应
  • 503 Server Unavailable:服务器暂时无法处理请求,恢复时间在 Retry-After

Conditional Get (条件 GET)

用户访问过该网页,再次访问。

GET 头部带有 If-Modified-Since:,若响应 304,则直接使用浏览器的缓存。否则返回正常实体

持久连接

HTTP 1.0 中:客户端请求头添加 Connection: Keep-alive,服务端同样在响应头中添加,保持连接

HTTP 1.1 中:默认所有连接都是长连接,添加 Connection: Close 才关闭,设置

  • Keep-Alive: timeout=5, max=100:长连接保持 5s,最多接收 100 次请求后断开
  • 注意:Keep-Alive 连接也是无状态的
  • 传输结束的条件:传输的数据达到 Content-Length

HTTP Pipelining 管线化

批量提交 HTTP 请求,不排队等待响应才发送下一个请求:请求1 -> 响应1 -> 请求2 -> 响应2 -> 请求3 -> 响应3 变为 请求1 -> 请求2 -> 请求3 -> 响应1 -> 响应2 -> 响应3

  • 管线化机制仅 HTTP1.1 支持
  • 只支持 GET、POST

HTTP1.0 中发下一个请求前,必须等待响应。

会话追踪

HTTP 请求是无状态的协议,不会保存客户端信息,实现:

cookie

  • 服务端发送给客户端的一小段信息,客户端每次请求都会带上,在有效期内识别身份。
  • 分为保存在浏览器的临时 cookie 和保存在内存的永久 cookie
  • 可被禁用

session

  • 服务端创建 session 对象并用 sessionID 标识,将 sessionID 放到 cookie 中,发送给客户端
  • cookie 被禁用,session 也会生效

token(重写 URL)

  • 在 URL 中添加标识每个用户的 token(cookie 被禁用时,将 sessionID 重写到 URL 中)

HTTP 安全

CSRF 跨站请求伪造:攻击者知道所有参数、构造合法请求

伪造成其他用户,发起请求:如拼接恶意链接给其他用户点击。防范:

  • 关键操作限制 POST
  • 合适的使用验证码
  • 添加 token:发起请求时添加附加的 token 参数,值随机。服务端做验证
  • header refer:外部来源可拒绝请求
XSS 跨站脚本攻击:在客户端不知情的情况下运行 js 脚本。防范:
  • 过滤和转义用户的输入内容:htmlspecialchars()
  • 限制参数类型
  • 文件上传做大小、类型限制

TCP 协议

特点

  • 面向连接、可靠、字节流服务:有 TCP 缓冲,可切割较长数据块、累积较短数据块。与 UDP 每次数据报不同
  • 校验和、确认和重传机制来保证可靠传输
  • 动态改变滑动窗口来控制流量
  • 一对一的通信,不能用于多播个广播

应用:HTTP、FTP、SMTP、SSH(数据准确性要求高)

优点:稳定可靠

缺点:慢、效率低、占资源多、易攻击(DDOS)

三次握手 Three-way Handshake

客户端执行 connect() 主动连接:

1
2
3
4
5
6
A:听得到吗?
B:听得到,你能听到我吗?
A:可以,我们可以交流了233

前两次:保证 B 能接收到 A 的信息,并作出正确响应
第三次:为了防止 A 的延迟的连接请求,B 一直在等待 A 的数据而浪费资源

理解:传输的信道不是绝对可靠的。为了不可靠的信道上可靠的传输信息,最少要进行三次通信。

TCP Flags 标志位:

1
2
3
4
5
6
SYN:synchronous 建立连接
ACK:acknowledgement 确认连接
PSH:push 推送
FIN:finish 释放连接(请求方数据已发送完毕)
RST: reset 重置(复位请求)
URG: urgent 紧急
  • SYN = 1 & ACK = 0:请求连接报文
  • SYN = 1 & ACK = 1:同意建立连接的响应报文
过程:

1

  • 第一次握手:客户端请求建立连接

    SYN = 1,Seq = x,进入 SYN_SEND 状态,等待服务端应答

  • 第二次握手:服务器允许建立连接

    SYN = 1,ACK = x+1,Seq = y,进入 SYNC_RCVD 状态,等待客户端确认

  • 第三次握手:客户端确认建立连接

    ACK = y + 1,连接建立。双方进入 ESTABLIASHED 状态

四次挥手 Four-way handshake

服务端和客户端均可主动断开连接:服务端、客户端均需确认对方无数据再发送2

  • 第一次握手:无数据再发送,主动关闭连接

    发送 FIN 报文,等待对法发送 ACK 报文,进入 FIN_WAIT_1 状态

  • 第二次握手:同意关闭连接

    发送 ACK 报文确认可关闭,并将未发送完毕的数据推送给对方

  • 第三次握手:请求对方关闭连接

    发送 FIN 报文,等待 ACK 报文

  • 第四次握手:关闭

    发送 ACK,进入 TIME_WAIT 状态,过 2 MSL(最大分段生存时间)未收到重传信息,直接关闭。

注意: 中间直接发送 ACK + FIN,则主动方会直接跳过 FIN_WAIT_2 状态

TCP Keep-Alive 机制(心跳包)

数据交互完毕后,一方主动释放连接。但出现意外时,TCP 连接不能及时释放,导致要维护很多半打开的连接。

实现:定时(半秒等)给对方发一个探测包,若收到 ACK 则认为连接存活,若 重试一定次数 都没收到回应则直接丢弃该 TCP 连接。

在我的 B 站直播间数据爬虫 抓取时,就需要每隔半分钟给 B 站的弹幕服务器发一个心跳包,否则连接会在一分钟后断开。

UDP 协议

特点

  • 不可靠:没有确认、超时重传、序列号机制:UDP 数据报不保证能送达、不保证数据报的顺序
  • 无需建立连接:创建 UDP 连接前无需握手创建连接
  • 一次发送一个报文,给定报文长度:过长 IP 层会分片
  • 支持多播和广播

应用:DNS、流媒体(速度要求 > 质量要求)

优点:快、比 TCP 稍安全

缺点:不可靠、不稳定

TCP UDP
报文 面向字节流 面向报文
双工性 全双工 一对一、一对多、多对一、多对多
流量控制 滑动窗口
拥塞控制 快重传、快恢复
传输速度

IP 协议

地址分类

IPv4 用点分十进制表示,IP 地址 = 网络地址 + 主机地址(层次)

全零 0.0.0.0 :本主机、全一:255.255.255.255 当前子网的广播地址

img

A 类:8 - 1(0) = 7 位网络号

  • 主机A的地址为:58.1.2.3/8,前面58位于1~127之间,所以这是一个A类地址.
  • 跟主机A位于同一个网络中的IP地址有 : 58.0.0.1 ~ 58.255.255.254,一共有 2^24-2个
  • 不包括:58.0.0.0(网络地址)跟58.255.255.255(广播地址)

B 类:16 - 2(10) = 14 位网络号

C 类:24 - 3(110) = 21 位网络号

子网掩码

用子网掩码划分一个 IP 的网络地址和主机地址。

1
IP & 子网掩码 = 网络地址

192.168.1.1/24192.168.1.1/255.255.255.0 的简写,前 24 位为网络号

子网划分:将大的整体网络划为小的子网络

Socket 编程

socket:三元组(IP 地址、协议、端口号)标识网络中唯一的进程,在 unix 中是文件

socket 使用了门面 Facade 模式:外部与内部的通信必须经过 facade

socket 隐藏了 TCP/IP 细节,开放交互接口。有:

1
2
3
4
5
6
7
socket()	// 创建套接字
bind() // 将套接字绑定到服务器地址上
listen() // 等待连接请求
accept() // 允许连接
read() // 读数据
write() // 写数据
close() // 关闭连接

搭建简单的 Server:

  • 等待连接
  • 建立连接
  • 接收请求:读取 HTTP 请求报文
  • 处理请求:访问资源(文件)
  • 构建响应:创建 HTTP 响应报文
  • 发送响应

数据结构

1
2
3
4
5
6
7
8
9
└── 数据结构
├── 数组
├── 链表
├── 栈
├── 队列
├── 哈希表
├── 二叉树
├── 堆
└── 字典树 trie

参考资料:常见数据结构及其多种实现的可视化

数组 Array

元素在内存中连续存放。每个元素占用内存相同,可通过下标算出元素位置快速访问

优点:访问快 O(1)

缺点:增加、删除元素需要移动大量元素,慢 O(N)

场景:快速访问元素,很少插入和删除元素

数组 链表
内存分配、元素存储位置 静态分配内存(栈,系统自动分配) 动态分配内存(堆,申请和管理麻烦)
分配方式 系统自动分配、速度快 自己申请和管理、new 慢
大小 编译时确定,具体值 是不连续的内存区域

链表 Linked List

元素在内存中不是连续存放。元素间通过指向指针联系在一起,访问元素必须从第一个元素开始遍历查找

优点:插入、删除元素只需改变指针,快 O(1)

缺点:访问慢 O(N)

场景:经常插入、删除元素

分类

  • 单向链表:节点仅指向下一节点,最后一个节点指向 nil
  • 双向链表:每个节点有 2 个指针 prenext,最后一个节点的 next 指向 nil
  • 循环链表:单链表 + 最后一个节点指向第一个节点

时间复杂度

  • 查找:O(N)
  • 插入、移除:O(1)

栈 Stack

元素遵循后进先出(LIFO)原则。元素仅在表尾(栈顶)进行插入(入栈 push)、删除(出栈 pop

实现

  • 单链表实现:保存头节点的指针,在头节点前入栈、头节点上出栈
  • 数组实现:直接操作数组最后一个元素,可能出现数组溢出:
    • 空栈(空数组)上 pop()
    • 满栈(满数组)上 push(),加倍数组大小

时间复杂度

  • 查找:O(N)
  • 插入、删除:O(1)

队列

元素遵循先进先出(FIFO)原则。元素在一端插入(进队列 enqueue)、另一端删除(出队列 dequeue)

出队列元素是在队列中存在时间最长的元素。

实现

  • 单链表实现:保存指向首尾节点的指针,从链表尾进队列,链表头出队列
  • 数组实现:修改 arr[0] 进队列,修改 arr[len - 1] 出队列

时间复杂度

  • 查找:O(N)
  • 插入、删除:O(1)

哈希表

散列表:根据 key 键值直接访问数据的内存地址

hash(data) = key:hash() 是一类算法,处理任意长度的 data,得到定长的 key 值,过程不可逆。

若 data 是数据集,则 key 也是数据集,将 keys 与原始数据一一映射就得到哈希表,即 M[key] = data

二叉树

img

  • 满二叉树:深度为 k 且有 2^k -1 个节点的二叉树
  • 完全二叉树:最后一层只缺右边节点,其它层节点数已为最大值

遍历方式(栈实现较好)

img

  • 前序遍历:根 -> 左 -> 右:abdefgc
  • 中序遍历:左 -> 根 -> 右:debgfac
  • 后序遍历:左 -> 右 ->根:edgfbac

二叉搜索树

性质:左子节点均小于根节点、右子节点均大于根节点

复杂度

  • 搜索:O(log(n))
  • 插入和删除:O(log(n))

算法

1
2
3
4
└── 算法
├── 排序
├── 查找
└── 字符串算法

参考资料:常见算法的过程可视化常见算法的 Go 实现

排序

img

数据大小 n,好的复杂度 O(logN),坏的 O(N2)

应用场景

  • n 较小:直接插入排序
  • 基本有序:冒泡排序、直接插入
  • n 较大:快速排序、归并排序、堆排序等 O(NlogN)

交换排序一:冒泡排序

img

过程

  • 比较相邻元素,前大于后就交换。
  • 遍历操作第 1~N 个元素,此时 第 N 个元素最大。
  • 遍历操作 1~N-1,重复遍历并比较、交换。…
  • 嵌套遍历结束即完成排序。

优化

  • 设置 flag,发生交换设为 true,某趟提前排序完毕则为 false,直接退出。
  • 记录每轮最后发生排序的位置,下轮遍历到此处即可。

分析

情况 复杂度
最佳情况:已有序,全遍历 1 次 O(N)
最坏情况:反序,全遍历 N 次 O(N^2)
平均情况 O(N^2)

交换排序二:快速排序

img

过程

  • 选取基准:从数组中选一个数(第一个)作为基准
  • 分区:遍历数组,比基准更小的值放在基准前边、更大的在后边
  • 递归:递归的在分区后的数组中,再选基准、分区

分析

情况 复杂度
最佳 O(logN)
最坏(数组反序、数组元素全部相同) O(N^2)
平均 O(logN)

选择排序一:简单选择排序

img

过程

  • 遍历第 1 次:找出最小的元素置于第 1 位。…
  • 遍历第 N-1 次:排序完毕

分析

情况 复杂度
最佳、最差、平均 O(N^2)

选择排序二:堆排序

img

过程

  • 将无序的数组构建成二叉树
  • 二叉树整理为堆(完全二叉树)
  • 输出根结点,再次整理堆输出根结点…

分析

情况 复杂度
最佳、最差、平均 O(logN)

插入排序一:直接插入排序

img

过程

  • 第一个元素(认为已排序好)
  • 从第二个元素开始,往后选择元素向前遍历,找到合适的位置插入
  • 遍历到最后一个位置,排序完毕

分析

情况 复杂度
最佳(数组升序) O(N)
最坏(数组反序) O(N^2)
平均 O(N^2)

插入排序二:希尔排序

img

过程:选择一个固定(动态)的步长,对步长内的元素进行直接插入排序

分析

情况 复杂度
最佳、最坏、平均(都要步长分割、遍历) O(NlogN)

归并排序

img

过程

  • 将长度为 N 的序数组分为 N / 2 的子数组
  • 递归划分子数组,子数组内部排序

分析

情况 复杂度
最佳 O(N)
最坏 O(NlogN)
平均 O(NlogN)

基数排序(非比较)

img

过程

  • 获取数组中最大值的位数 b
  • 从 1 ~ b 遍历组合位数相同的元素

分析

情况 复杂度
最佳、最坏、平均 O(N * b)

查找

无序查找:顺序查找

  • 顺序扫描序列,依次比较值
  • 复杂度:O(N)

有序查找(无序序列需要提前排序)

二分查找(折半查找)

  • 找到中间节点比较值,相等则查找到,更小则继续对左边折半查找,更大则向右边
  • 查找比对点:mid = (low + high) / 2mid = low + (high - low) / 2
  • 复杂度:比较次数,O(log2 N)

插值查找

  • 动态的改进二分查找的查找点,使其更接近区间
  • 查找对比点:mid = low + (key - arr[low]) / (arr[high] - arr[low]) * (high - low)
  • 复杂度:O(log2(log2N))

二叉查找树

  • 中序遍历获得排序好的数组
  • 复杂度:一般 O(log2N),最坏情况 单支树 O(N)

平衡二叉查找树(AVL 树)

  • 任何节点的 2 棵子树,高度差最多为 1
  • 平衡:根结点到任意一个叶子节点,距离都相等

操作系统

体系结构

  • 机器码:最高位 0 为正、1 为 负

  • 原码:符号位 + 真值绝对值的二进制

  • 反码:正数反码就是本身、负数除符号位外取反

  • 补码:正数补码就是本身、负数除符号位外取反 + 1

    1
    2
    [+1] = [00000001]原 = [00000001]反 = [00000001]补
    [-1] = [10000001]原 = [11111110]反 = [11111111]补
  • 1 byte = 8 bits,1 word = 2 byte(16位)= 4 byte(32位机)& 字是计算机数据处理和运算的单位

  • 字节序:占用内存超过 1 byte 的数据在内存中的存放顺序

    • 小端字节序:低字节数据放在内存低地址

    • 大端字节序:低字节数据放在内存高地址(符合阅读习惯,网络中数据传输的协议,即网络字节序)
      B 站直播数据抓取 中,请求数据的打包格式就是大端字节序,否则连接到弹幕服务器。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      0x12345678 的存储:

      Big Endian
      低地址 高地址
      ---------------------------------------------------->
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      | 12 | 34 | 56 | 78 |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

      Little Endian
      低地址 高地址
      ---------------------------------------------------->
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      | 78 | 56 | 34 | 12 |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

基础

操作系统功能:文件管理、存储管理、输入输出管理、作业管理、进程管理

中断

  • CPU 暂停执行当前程序,去执行中断程序
  • 中断的优先级指明处理的紧急程度,如:机器错误 > 时钟 > 磁盘 > 网络设备 > 终端 > 软件中断

系统调用

  • 2 个级别:核心态、用户态
  • 程序执行一般在 user model,需要使用操作系统服务(创建、读写文件),请求切换到 kernel model 执行
  • 核心态能存取用户、内核的指令和数据,用户态只能存取用户自己的指令和数据

中断和系统调用的关系:程序申请核心态时,将产生一个软件中断,系统将处理

并发多任务

多道程序:程序运行, CPU 空闲时(等待 IO),此时 CPU 去运行其他程序

分时系统:改进后的多道程序,程序运行一段时间后会主动让出 CPU

多任务系统:操作系统从底层接管所有硬件资源,程序以进程运行,程序运行超时会被强制暂停

进程

运行中的程序

4 个地址空间

  • 文本域:要执行的代码
  • 数据域:存放变量、程序执行期间动态分配的内存
  • 堆栈:存放本地变量和指令

3 种状态

  • 等待态:IO
  • 就绪态:等待系统分配 CPU 运行
  • 运行态:占用 CPU 正在处理

4 种进程间通信

  • 消息传递:pipe管道
  • 同步:信号量、读写锁
  • 内存共享
  • 远程过程调用:RPC

死锁:多个进程因循环等待资源而都无法执行

线程

轻量级进程

  • 解决问题:很多不同的进程需要共享同样的资源(文件),切换进程的成本很高
  • 是可以独立运行的单位,切换快速、开销小 & 可并发执行
  • 共享进程的资源:在同一进程中的线程因为有相应的地址空间,可以共享进程已打开的文件、定时器等

协程

  • 微线程,coroutine:用户级的线程
  • 线程是抢占式调度、协程是协同式调度,避免无意义的抢占,但是调度由用户决定

IO 多路复用

内核发现某个进程指定的 IO 准备读取,就会通知该进程,如客户端处理多个描述符时,大大减小创建和维护的开销。

并发与并行

  • 并发:多个操作可在重叠的时间运行
  • 并行:同一时刻有多条指令在执行,多核是并行的前提。

数据库

事务:一系列 SQL 集合

ACID 特性

  • 原子性 automatic:事务是原子工作单位,要么全部执行、要么全部不执行
  • 一致性 consistency:数据库通过事务完成状态转变
  • 隔离性 isolation:事务提交前,对数据的影响是不可见的
  • 持久性 duration:事务完成后,对数据的影响是持久的

4 种隔离级别

  • 脏读:一个事务读取了另一个事务尚未提交的修改
  • 非重复读:一个事务对同一行数据读取两次,得到不同结果
  • 幻读:事务在操作过程中进行了两次查询,第二次的结果包含了第一次未出现的新数据
  • 丢失修改:当两个事务更新相同的数据源,第一个事务提交,第二个撤销。那么第一个也要撤销

3 种实现隔离的锁

  • 共享锁 S 锁:只读 SELECT 操作,锁定共享资源,阻止其他用户写数据
  • 更新锁 U 锁:阻止其他用户更新数据
  • 独占锁 X 锁:一次只能有一个独占锁占用一个资源,阻止添加其他所有锁。有效防止脏读

索引

优点

  • 大大加快检索速度
  • 加快表之间的关联
  • 唯一性索引 UNIQUE:保证行数据的唯一性

缺点

  • 创建和维护消耗时间和物理空间

场景

  • 用在经常需要连接的字段:如外键,加快连接速度
  • 用在经常需要排序的字段:索引已排序,直接利用,加快排序时间

OOP

三个基本特征:封装、继承、多态

封装

  • 提取对象的特征,抽象成类。
  • 外部只能访问 public 的属性和方法。private 的属性和方法内部调用,设置 getter、setter 开放接口允许外部访问和修改私有数据,protected 的数据和方法子类继承和调用

继承:使用现有类的所有功能。

多态:子类覆盖父类的同名方法

Golang 面试题

问答类

1. 在 Go 中如何使用多行字符串?

使用反引号 来包含多行字串,或使用 + 来连接多行字符串(注意换行会包含\n,缩进会包含 \t,空格没有转义符):

1
2
3
4
5
6
7
8
9
func main() {
str1 := `
line1
line2
`
str2 := "\n line1\n\t" +
"line2\n"
fmt.Println(str1 == str2) // true
}

2. 如何获取命令行的参数?

有两种方法:

使用 os 库,如:

1
2
3
4
5
6
7
8
func main() {
args := os.Args
if args == nil { // 校验参数并输出提示信息
return
}
fmt.Printf("%T\n", args)
fmt.Printf("%v\n", args)
}

可以看出 os.Args 接收到的参数是 string slice,元素分别是运行的程序名、多个参数值:

img

使用 flag 库,步骤:

  • 定义各个参数的类型、名字、默认值与提示信息
  • 解析
  • 获取参数值
1
2
3
4
5
6
7
8
9
10
func main() {
name := flag.String("name", "", "Your name")
var age int
flag.IntVar(&age, "age", -1, "Your age")

flag.Parse()

println("name", *name)
println("age", age)
}

注意上边获取参数值的两种方式,使用时也有所不同:

1
2
func Int(name string, value string, usage string) *string // 返回地址
func IntVar(p *int, name string, value int, usage string) // 修改第一个参数值

img

3. 如何在不输出的情况下格式化字符串?

使用 func Sprintf(format string, a ...interface{}) string 即可,常用在手动组合 SQL 语句上:

1
2
3
4
5
6
7
func main() {
fmt.Println(formatSQL(20))
}

func formatSQL(id int) string {
return fmt.Sprintf("SELECT * FROM users WHERE id=%d", id)
}

4. 如何交换两个变量的值?

直接使用元组(tuple)赋值即可:

1
a, b = b, a

注意元组赋值是对应有序赋值的:

1
2
3
4
5
a, b, c = b, c, a // 交换三个变量的值

a := 1
b := 2
a, b, a = b, a, b // a = 2, b = 1

5. 如何复制 slice、map 和 interface 的值?

slice:

1
2
3
4
5
6
7
8
9
10
func main() {
names := []string{"Tom", "Jerry"}
nums := []string{"one", "two", "three"}
pNames := names // 确认 names 被更新

// names = nums // 直接赋值

// fmt.Println(copy(names, nums)) // 使用 copy
fmt.Println(names, nums, pNames)
}
  • 直接赋值, 底层数组将不会更新:

img

  • 使用 copy()
    返回值是 min(len(names), len(src)),只会拷贝前两个元素,pNames 的值显示 names 的底层数组已被覆盖更新:

    img

map:

最简单的方法,遍历所有 key:

1
2
3
4
5
6
7
8
9
func main() {
src := map[string]bool{"key1": false, "key2": true}
dst := make(map[string]bool)

for key, value := range src { // 遍历所有 key
dst[key] = value
}
fmt.Println(dst)
}

interface:

Go 中没有内建的函数来直接拷贝 interface 的值,也不能直接赋值。如 2 个 struct 的字段完全一致,可以使用强制类型转换或反射来赋值。

参考:关于结构体复制问题Copying Interface Values In Go

6. 下边两种 slice 的声明有何不同?哪种更好?

1
2
var nums []int
nums := []int{}

第一种如果不使用 nums,就不会为其分配内存,更好(不使用编译也不会通过)。

写出程序运行输出的内容

1. 考察多个 defer 与 panic 的执行顺序

1
2
3
4
5
6
7
8
9
10
11
func main() {
deferCall()
}

func deferCall() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()

panic("触发异常")
}

defer 可以类比为析构函数,多个 defer 本身的执行是栈 LIFO 先进后出的顺序,代码抛出的 panic 如果在所有 defer 中都不使用 recover 恢复,则直接退出程序。

如果手动使用 os.Exit() 退出,则 defer 不执行。

img

2. 考察 defer 与 return 的执行顺序

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
func main() {
fmt.Println(double1(5))
fmt.Println(double1(6))
fmt.Println()
fmt.Println(double2(5))
fmt.Println(double2(6))
}

// 匿名返回
// 加倍参数,若结果超过 10 则还原
func double1(v1 int) int {
var v2 int
defer func() {
if v2 > 10 {
v2 = v1 // v2 不会被修改
}
}()

v2 = v1 * 2
return v2
}

// 有名返回
func double2(v1 int)(v2 int) {
// v2 与函数一起被声明,在 defer 中能被修改
defer func() {
if v2 > 10 {
v2 = v1 // v2 被修改
}
}()

v2 = v1 * 2
return
}

注意 return var 会分为三步执行:

return 语句为 var 赋值

  • 匿名返回值函数:先声明,再赋值
  • 有名返回值函数:直接赋值

检查是否存在 defer 语句:逆序执行多条 defer,有名返回函数可能会再次修改 var

真正返回 var 到调用处

img

3. 考察 goroutine 的传值方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
runtime.GOMAXPROCS(1) // 强制使多个 goroutine 串行执行
wg := sync.WaitGroup{}
wg.Add(10)

for i := 0; i < 5; i++ {
go func() {
fmt.Println("i: ", i)
wg.Done()
}()
// time.Sleep(1 * time.Second) // 此时将顺序输出 1 2 3 4 5
}

for i := 0; i < 5; i++ {
go func(i int) {
fmt.Println("i: ", i)
wg.Done()
}(i)
}
wg.Wait()
}

第一个 for 循环:以极快的速度分配完 5 个 goroutine,此时 i 的值为 5,gouroutine 得到的 i 都是 5

第二个 for 循环:每次都会将 i 的值拷贝一份传给 goroutine,得到的 i 不同,输出不同

img

4. 考察 defer 参数的计算时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
a := 1
b := 2
defer add("A", a, add("B", a, b))
a = 0
defer add("C", a, add("D", a, b))
b = 1
}


func add(desc string, a, b int) int {
sum := a + b
fmt.Println(desc, a, b, sum)
return sum
}

defer 语句会计算好 func 的参数,再放入执行栈中。

注意第 7 行:四个 defer func 的参数此时已是确定值,不再对 defer 中的 b 造成影响。

img

5. 考察 Go 的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type People struct{}

func (p *People) ShowA() {
fmt.Println("people showA")
p.ShowB()
}
func (p *People) ShowB() {
fmt.Println("people showB")
}


type Teacher struct {
People
}

func (t *Teacher) ShowB() {
fmt.Println("teacher showB")
}

func main() {
t := Teacher{}
t.ShowB()
t.ShowA()
}

第 13 行: Teacher 通过嵌入 People 来获取了 ShowA()showB()

第 16 行:Teacher 实现并覆盖了 showB()

第 24 行:调用未覆盖的 showA(),因为它的 receiver 依旧是 People,相当于 People 调用

img

前言

Redis 是一个高性能的 key-value 数据库,作为一个典型的内存数据库,高速读写性能并支持数据持久化是大多数人选择 Redis 的原因,当然 Redis 也有很多局限,即使现在升级至 5.0 在技术圈仍有很多不同的声音。Redis 监控可以通过 Keepalived 结合简单的脚本实现,也可以基于 Redis Sentinel 监控,如果需要横向扩展使用 Codis 或许是更加成熟稳定的方案。

Redis 主备同步配置实践

扩展阅读

Redis - https://redis.io/


Redis 简介

Redis is often referred as a data structures server. What this means is that Redis provides access to mutable data structures via a set of commands, which are sent using a server-client model with TCP sockets and a simple protocol. So different processes can query and modify the same data structures in a shared way.

Data structures implemented into Redis have a few special properties:

  • Redis cares to store them on disk, even if they are always served and modified into the server memory. This means that Redis is fast, but that is also non-volatile.
  • Implementation of data structures stress on memory efficiency, so data structures inside Redis will likely use less memory compared to the same data structure modeled using an high level programming language.
  • Redis offers a number of features that are natural to find in a database, like replication, tunable levels of durability, cluster, high availability.

Another good example is to think of Redis as a more complex version of memcached, where the operations are not just SETs and GETs, but operations to work with complex data types like Lists, Sets, ordered data structures, and so forth.

If you want to know more, this is a list of selected starting points:

Documentation

Latest stable version tar ball

http://download.redis.io/redis-stable.tar.gz

http://download.redis.io/releases/redis-5.0.2.tar.gz

http://download.redis.io/releases/redis-3.2.10.tar.gz

Browse source code

http://download.redis.io/redis-stable/

http://download.redis.io/redis-stable/README.md

Install

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# download
wget http://download.redis.io/redis-stable.tar.gz
tar xf redis-stable.tar.gz
cd redis-stable
# check packages
yum -y install gcc gcc-c++ tcl
# check version
./src/redis-cli -v
redis-cli 5.0.2
# start redis server
./src/redis-server redis.conf
# test
[root@localhost ~]# ./redis-stable/src/redis-cli
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> get foo
"bar"
127.0.0.1:6379>

Configuration

不理解的配置参数可以参考官方文档或者下面的中文翻译

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
# create directory
mkdir -p /data/running/redis-6389
mkdir -p /var/log/redis/

# copy custom scripts
-rwxr-xr-x 1 root root 266 Nov 28 19:11 change_redis.py
-rwxr-xr-x 1 root root 323 Nov 28 19:11 check_redis.sh
-rwxr-xr-x 1 root root 20 Nov 28 19:11 connect_redis.sh
-rw-r--r-- 1 root root 74 Nov 28 19:11 keep_alived_state
-rwxr-xr-x 1 root root 829 Nov 28 19:11 keepalived.state.sh
-rwxr-xr-x 1 root root 173376 Nov 28 19:11 redis-cli
-rw-r----- 1 root root 46666 Nov 28 19:11 redis.conf
-rwxr-xr-x 1 root root 979464 Nov 28 19:11 redis-server
-rwxr-xr-x 1 root root 74 Nov 28 19:11 start_redis.sh
-rwxr-xr-x 1 root root 38 Nov 28 19:11 stop_redis.sh

# set custom values
[root@sg-gop-10-71-12-78 redis-6389]# grep -Ev "^$|#" redis.conf
protected-mode no
port 6389
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize yes
supervised no
pidfile /var/run/redis_6389.pid
loglevel notice
logfile /var/log/redis/redis-6389.log
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /data/running/redis-6389
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
maxclients 50000
maxmemory 400gb
maxmemory-policy allkeys-lru
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 0 0 0
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
aof-rewrite-incremental-fsync yes

# set slave
telnet 127.0.0.1 6389
slaveof 10.71.12.70 6389
# cancel slave
slaveof no one
info
# Replication
role:master
connected_slaves:1
slave0:ip=10.71.12.71,port=6389,state=online,offset=2283,lag=1
master_repl_offset:2283
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:2282
# Replication
role:slave
master_host:10.71.12.70
master_port:6389
master_link_status:up
master_last_io_seconds_ago:5
master_sync_in_progress:0
slave_repl_offset:2059
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

redis.conf 配置英文原版

最简单的模式只需要修改 daemonize yes,然后备机使用 slaveof 命令设置即可

http://download.redis.io/redis-stable/redis.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# Redis 默认配置
[root@localhost redis-stable]# grep -Ev "^$|#" redis.conf
bind 127.0.0.1
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 16
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ./
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
replica-priority 100
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
appendonly no
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes

redis.conf 配置中文翻译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
#redis.conf
# Redis configuration file example.
# ./redis-server /path/to/redis.conf

################################## INCLUDES ###################################
# 这在你有标准配置模板但是每个 redis 服务器又需要个性设置的时候很有用。
include /path/to/local.conf
include /path/to/other.conf

################################ GENERAL #####################################

# 是否在后台执行,yes:后台运行;no:不是后台运行(老版本默认)
daemonize yes

#3.2 里的参数,是否开启保护模式,默认开启。要是配置里没有指定 bind 和密码。开启该参数后,redis 只会本地进行访问,拒绝外部访问。要是开启了密码 和 bind,可以开启。否 则最好关闭,设置为 no。
protected-mode yes

#redis 的进程文件
pidfile /var/run/redis/redis-server.pid

#redis 监听的端口号。
port 6379

# 此参数确定了 TCP 连接中已完成队列(完成三次握手之后) 的长度, 当然此值必须不大于 Linux 系统定义的 / proc/sys/net/core/somaxconn 值,默认是 511,而 Linux 的默认参数值是 128。当系统并发量大并且客户端速度缓慢的时候,可以将这二个参数一起参考设定。该内核参数默认值一般是 128,对于负载很大的服务程序来说大大的不够。一般会将它修改为 2048 或者更大。在 / etc/sysctl.conf 中添加: net.core.somaxconn = 2048,然后在终端中执行 sysctl -p。
tcp-backlog 511

# 指定 redis 只接收来自于该 IP 地址的请求,如果不进行设置,那么将处理所有请求
bind 127.0.0.1

# 配置 unix socket 来让 redis 支持监听本地连接。
# unixsocket /var/run/redis/redis.sock
# 配置 unix socket 使用文件的权限
# unixsocketperm 700

# 此参数为设置客户端空闲超过 timeout,服务端会断开连接,为 0 则服务端不会主动断开连接,不能小于 0。
timeout 0

#tcp keepalive 参数。如果设置不为 0,就使用配置 tcp 的 SO_KEEPALIVE 值,使用 keepalive 有两个好处: 检测挂掉的对端。降低中间设备出问题而导致网络看似连接却已经与对端端口的问题。在 Linux 内核中,设置了 keepalive,redis 会定时给对端发送 ack。检测到对端关闭需要两倍的设置值。
tcp-keepalive 0

# 指定了服务端日志的级别。级别包括:debug(很多信息,方便开发、测试),verbose(许多有用的信息,但是没有 debug 级别信息多),notice(适当的日志级别,适合生产环境),warn(只有非常重要的信息)
loglevel notice

# 指定了记录日志的文件。空字符串的话,日志会打印到标准输出设备。后台运行的 redis 标准输出是 / dev/null。
logfile /var/log/redis/redis-server.log

# 是否打开记录 syslog 功能
# syslog-enabled no

#syslog 的标识符。
# syslog-ident redis

# 日志的来源、设备
# syslog-facility local0

# 数据库的数量,默认使用的数据库是 DB 0。可以通过”SELECT “命令选择一个 db
databases 16

################################ SNAPSHOTTING ################################
# 快照配置
# 注释掉“save” 这一行配置项就可以让保存数据库功能失效
# 设置 sedis 进行数据库镜像的频率。
# 900 秒(15 分钟)内至少 1 个 key 值改变(则进行数据库保存 -- 持久化)
# 300 秒(5 分钟)内至少 10 个 key 值改变(则进行数据库保存 -- 持久化)
# 60 秒(1 分钟)内至少 10000 个 key 值改变(则进行数据库保存 -- 持久化)
save 900 1
save 300 10
save 60 10000

# 当 RDB 持久化出现错误后,是否依然进行继续进行工作,yes:不能进行工作,no:可以继续进行工作,可以通过 info 中的 rdb_last_bgsave_status 了解 RDB 持久化是否有错误
stop-writes-on-bgsave-error yes

# 使用压缩 rdb 文件,rdb 文件压缩使用 LZF 压缩算法,yes:压缩,但是需要一些 cpu 的消耗。no:不压缩,需要更多的磁盘空间
rdbcompression yes

# 是否校验 rdb 文件。从 rdb 格式的第五个版本开始,在 rdb 文件的末尾会带上 CRC64 的校验和。这跟有利于文件的容错性,但是在保存 rdb 文件的时候,会有大概 10% 的性能损耗,所以如果你追求高性能,可以关闭该配置。
rdbchecksum yes

#rdb 文件的名称
dbfilename dump.rdb

# 数据目录,数据库的写入会在这个目录。rdb、aof 文件也会写在这个目录
dir /var/lib/redis

################################# REPLICATION #################################
# 复制选项,slave 复制对应的 master。
# slaveof <masterip> <masterport>

# 如果 master 设置了 requirepass,那么 slave 要连上 master,需要有 master 的密码才行。masterauth 就是用来配置 master 的密码,这样可以在连上 master 后进行认证。
# masterauth <master-password>

# 当从库同主机失去连接或者复制正在进行,从机库有两种运行方式:1) 如果 slave-serve-stale-data 设置为 yes(默认设置),从库会继续响应客户端的请求。2) 如果 slave-serve-stale-data 设置为 no,除去 INFO 和 SLAVOF 命令之外的任何请求都会返回一个错误”SYNC with master in progress”。
slave-serve-stale-data yes

# 作为从服务器,默认情况下是只读的(yes),可以修改成 NO,用于写(不建议)。
slave-read-only yes

# 是否使用 socket 方式复制数据。目前 redis 复制提供两种方式,disk 和 socket。如果新的 slave 连上来或者重连的 slave 无法部分同步,就会执行全量同步,master 会生成 rdb 文件。有 2 种方式:disk 方式是 master 创建一个新的进程把 rdb 文件保存到磁盘,再把磁盘上的 rdb 文件传递给 slave。socket 是 master 创建一个新的进程,直接把 rdb 文件以 socket 的方式发给 slave。disk 方式的时候,当一个 rdb 保存的过程中,多个 slave 都能共享这个 rdb 文件。socket 的方式就的一个个 slave 顺序复制。在磁盘速度缓慢,网速快的情况下推荐用 socket 方式。
repl-diskless-sync no

#diskless 复制的延迟时间,防止设置为 0。一旦复制开始,节点不会再接收新 slave 的复制请求直到下一个 rdb 传输。所以最好等待一段时间,等更多的 slave 连上来。
repl-diskless-sync-delay 5

#slave 根据指定的时间间隔向服务器发送 ping 请求。时间间隔可以通过 repl_ping_slave_period 来设置,默认 10 秒。
# repl-ping-slave-period 10

# 复制连接超时时间。master 和 slave 都有超时时间的设置。master 检测到 slave 上次发送的时间超过 repl-timeout,即认为 slave 离线,清除该 slave 信息。slave 检测到上次和 master 交互的时间超过 repl-timeout,则认为 master 离线。需要注意的是 repl-timeout 需要设置一个比 repl-ping-slave-period 更大的值,不然会经常检测到超时。
# repl-timeout 60

# 是否禁止复制 tcp 链接的 tcp nodelay 参数,可传递 yes 或者 no。默认是 no,即使用 tcp nodelay。如果 master 设置了 yes 来禁止 tcp nodelay 设置,在把数据复制给 slave 的时候,会减少包的数量和更小的网络带宽。但是这也可能带来数据的延迟。默认我们推荐更小的延迟,但是在数据量传输很大的场景下,建议选择 yes。
repl-disable-tcp-nodelay no

# 复制缓冲区大小,这是一个环形复制缓冲区,用来保存最新复制的命令。这样在 slave 离线的时候,不需要完全复制 master 的数据,如果可以执行部分同步,只需要把缓冲区的部分数据复制给 slave,就能恢复正常复制状态。缓冲区的大小越大,slave 离线的时间可以更长,复制缓冲区只有在有 slave 连接的时候才分配内存。没有 slave 的一段时间,内存会被释放出来,默认 1m。
# repl-backlog-size 5mb

#master 没有 slave 一段时间会释放复制缓冲区的内存,repl-backlog-ttl 用来设置该时间长度。单位为秒。
# repl-backlog-ttl 3600

# 当 master 不可用,Sentinel 会根据 slave 的优先级选举一个 master。最低的优先级的 slave,当选 master。而配置成 0,永远不会被选举。
slave-priority 100

#redis 提供了可以让 master 停止写入的方式,如果配置了 min-slaves-to-write,健康的 slave 的个数小于 N,mater 就禁止写入。master 最少得有多少个健康的 slave 存活才能执行写命令。这个配置虽然不能保证 N 个 slave 都一定能接收到 master 的写操作,但是能避免没有足够健康的 slave 的时候,master 不能写入来避免数据丢失。设置为 0 是关闭该功能。
# min-slaves-to-write 3

# 延迟小于 min-slaves-max-lag 秒的 slave 才认为是健康的 slave。
# min-slaves-max-lag 10

# 设置 1 或另一个设置为 0 禁用这个特性。
# Setting one or the other to 0 disables the feature.
# By default min-slaves-to-write is set to 0 (feature disabled) and
# min-slaves-max-lag is set to 10.

################################## SECURITY ###################################
#requirepass 配置可以让用户使用 AUTH 命令来认证密码,才能使用其他命令。这让 redis 可以使用在不受信任的网络中。为了保持向后的兼容性,可以注释该命令,因为大部分用户也不需要认证。使用 requirepass 的时候需要注意,因为 redis 太快了,每秒可以认证 15w 次密码,简单的密码很容易被攻破,所以最好使用一个更复杂的密码。
# requirepass foobared

# 把危险的命令给修改成其他名称。比如 CONFIG 命令可以重命名为一个很难被猜到的命令,这样用户不能使用,而内部工具还能接着使用。
# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52

# 设置成一个空的值,可以禁止一个命令
# rename-command CONFIG ""
################################### LIMITS ####################################

# 设置能连上 redis 的最大客户端连接数量。默认是 10000 个客户端连接。由于 redis 不区分连接是客户端连接还是内部打开文件或者和 slave 连接等,所以 maxclients 最小建议设置到 32。如果超过了 maxclients,redis 会给新的连接发送’max number of clients reached’,并关闭连接。
# maxclients 10000

#redis 配置的最大内存容量。当内存满了,需要配合 maxmemory-policy 策略进行处理。注意 slave 的输出缓冲区是不计算在 maxmemory 内的。所以为了防止主机内存使用完,建议设置的 maxmemory 需要更小一些。
# maxmemory <bytes>

# 内存容量超过 maxmemory 后的处理策略。
#volatile-lru:利用 LRU 算法移除设置过过期时间的 key。
#volatile-random:随机移除设置过过期时间的 key。
#volatile-ttl:移除即将过期的 key,根据最近过期时间来删除(辅以 TTL)
#allkeys-lru:利用 LRU 算法移除任何 key。
#allkeys-random:随机移除任何 key。
#noeviction:不移除任何 key,只是返回一个写错误。
# 上面的这些驱逐策略,如果 redis 没有合适的 key 驱逐,对于写命令,还是会返回错误。redis 将不再接收写请求,只接收 get 请求。写命令包括:set setnx setex append incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby getset mset msetnx exec sort。
# maxmemory-policy noeviction

#lru 检测的样本数。使用 lru 或者 ttl 淘汰算法,从需要淘汰的列表中随机选择 sample 个 key,选出闲置时间最长的 key 移除。
# maxmemory-samples 5

############################## APPEND ONLY MODE ###############################
# 默认 redis 使用的是 rdb 方式持久化,这种方式在许多应用中已经足够用了。但是 redis 如果中途宕机,会导致可能有几分钟的数据丢失,根据 save 来策略进行持久化,Append Only File 是另一种持久化方式,可以提供更好的持久化特性。Redis 会把每次写入的数据在接收后都写入 appendonly.aof 文件,每次启动时 Redis 都会先把这个文件的数据读入内存里,先忽略 RDB 文件。
appendonly no

#aof 文件名
appendfilename "appendonly.aof"

#aof 持久化策略的配置
#no 表示不执行 fsync,由操作系统保证数据同步到磁盘,速度最快。
#always 表示每次写入都执行 fsync,以保证数据同步到磁盘。
#everysec 表示每秒执行一次 fsync,可能会导致丢失这 1s 数据。
appendfsync everysec

# 在 aof 重写或者写入 rdb 文件的时候,会执行大量 IO,此时对于 everysec 和 always 的 aof 模式来说,执行 fsync 会造成阻塞过长时间,no-appendfsync-on-rewrite 字段设置为默认设置为 no。如果对延迟要求很高的应用,这个字段可以设置为 yes,否则还是设置为 no,这样对持久化特性来说这是更安全的选择。设置为 yes 表示 rewrite 期间对新写操作不 fsync, 暂时存在内存中, 等 rewrite 完成后再写入,默认为 no,建议 yes。Linux 的默认 fsync 策略是 30 秒。可能丢失 30 秒数据。
no-appendfsync-on-rewrite no

#aof 自动重写配置。当目前 aof 文件大小超过上一次重写的 aof 文件大小的百分之多少进行重写,即当 aof 文件增长到一定大小的时候 Redis 能够调用 bgrewriteaof 对日志文件进行重写。当前 AOF 文件大小是上次日志重写得到 AOF 文件大小的二倍(设置为 100)时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
# 设置允许重写的最小 aof 文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写
auto-aof-rewrite-min-size 64mb

#aof 文件可能在尾部是不完整的,当 redis 启动的时候,aof 文件的数据被载入内存。重启可能发生在 redis 所在的主机操作系统宕机后,尤其在 ext4 文件系统没有加上 data=ordered 选项(redis 宕机或者异常终止不会造成尾部不完整现象。)出现这种现象,可以选择让 redis 退出,或者导入尽可能多的数据。如果选择的是 yes,当截断的 aof 文件被导入的时候,会自动发布一个 log 给客户端然后 load。如果是 no,用户必须手动 redis-check-aof 修复 AOF 文件才可以。
aof-load-truncated yes

################################ LUA SCRIPTING ###############################
# 如果达到最大时间限制(毫秒),redis 会记个 log,然后返回 error。当一个脚本超过了最大时限。只有 SCRIPT KILL 和 SHUTDOWN NOSAVE 可以用。第一个可以杀没有调 write 命令的东西。要是已经调用了 write,只能用第二个命令杀。
lua-time-limit 5000

################################ REDIS CLUSTER ###############################
# 集群开关,默认是不开启集群模式。
# cluster-enabled yes

# 集群配置文件的名称,每个节点都有一个集群相关的配置文件,持久化保存集群的信息。这个文件并不需要手动配置,这个配置文件有 Redis 生成并更新,每个 Redis 集群节点需要一个单独的配置文件,请确保与实例运行的系统中配置文件名称不冲突
# cluster-config-file nodes-6379.conf

# 节点互连超时的阀值。集群节点超时毫秒数
# cluster-node-timeout 15000

# 在进行故障转移的时候,全部 slave 都会请求申请为 master,但是有些 slave 可能与 master 断开连接一段时间了,导致数据过于陈旧,这样的 slave 不应该被提升为 master。该参数就是用来判断 slave 节点与 master 断线的时间是否过长。判断方法是:
# 比较 slave 断开连接的时间和(node-timeout * slave-validity-factor) + repl-ping-slave-period
# 如果节点超时时间为三十秒, 并且 slave-validity-factor 为 10, 假设默认的 repl-ping-slave-period 是 10 秒,即如果超过 310 秒 slave 将不会尝试进行故障转移
# cluster-slave-validity-factor 10

#master 的 slave 数量大于该值,slave 才能迁移到其他孤立 master 上,如这个参数若被设为 2,那么只有当一个主节点拥有 2 个可工作的从节点时,它的一个从节点会尝试迁移。
# cluster-migration-barrier 1

# 默认情况下,集群全部的 slot 有节点负责,集群状态才为 ok,才能提供服务。设置为 no,可以在 slot 没有全部分配的时候提供服务。不建议打开该配置,这样会造成分区的时候,小分区的 master 一直在接受写请求,而造成很长时间数据不一致。
# cluster-require-full-coverage yes

################################## SLOW LOG ###################################
###slog log 是用来记录 redis 运行中执行比较慢的命令耗时。当命令的执行超过了指定时间,就记录在 slow log 中,slog log 保存在内存中,所以没有 IO 操作。
# 执行时间比 slowlog-log-slower-than 大的请求记录到 slowlog 里面,单位是微秒,所以 1000000 就是 1 秒。注意,负数时间会禁用慢查询日志,而 0 则会强制记录所有命令。
slowlog-log-slower-than 10000

# 慢查询日志长度。当一个新的命令被写进日志的时候,最老的那个记录会被删掉。这个长度没有限制。只要有足够的内存就行。你可以通过 SLOWLOG RESET 来释放内存。
slowlog-max-len 128

################################ LATENCY MONITOR ##############################
# 延迟监控功能是用来监控 redis 中执行比较缓慢的一些操作,用 LATENCY 打印 redis 实例在跑命令时的耗时图表。只记录大于等于下边设置的值的操作。0 的话,就是关闭监视。默认延迟监控功能是关闭的,如果你需要打开,也可以通过 CONFIG SET 命令动态设置。
latency-monitor-threshold 0

############################# EVENT NOTIFICATION ##############################
# 键空间通知使得客户端可以通过订阅频道或模式,来接收那些以某种方式改动了 Redis 数据集的事件。因为开启键空间通知功能需要消耗一些 CPU ,所以在默认配置下,该功能处于关闭状态。
#notify-keyspace-events 的参数可以是以下字符的任意组合,它指定了服务器该发送哪些类型的通知:
##K 键空间通知,所有通知以 __keyspace@__ 为前缀
##E 键事件通知,所有通知以 __keyevent@__ 为前缀
##g DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知
##$ 字符串命令的通知
##l 列表命令的通知
##s 集合命令的通知
##h 哈希命令的通知
##z 有序集合命令的通知
##x 过期事件:每当有过期键被删除时发送
##e 驱逐(evict) 事件:每当有键因为 maxmemory 政策而被删除时发送
##A 参数 g$lshzxe 的别名
# 输入的参数中至少要有一个 K 或者 E,否则的话,不管其余的参数是什么,都不会有任何 通知被分发。详细使用可以参考 http://redis.io/topics/notifications

notify-keyspace-events ""

############################### ADVANCED CONFIG ###############################
# 数据量小于等于 hash-max-ziplist-entries 的用 ziplist,大于 hash-max-ziplist-entries 用 hash
hash-max-ziplist-entries 512
#value 大小小于等于 hash-max-ziplist-value 的用 ziplist,大于 hash-max-ziplist-value 用 hash。
hash-max-ziplist-value 64

# 数据量小于等于 list-max-ziplist-entries 用 ziplist,大于 list-max-ziplist-entries 用 list。
list-max-ziplist-entries 512
#value 大小小于等于 list-max-ziplist-value 的用 ziplist,大于 list-max-ziplist-value 用 list。
list-max-ziplist-value 64

# 数据量小于等于 set-max-intset-entries 用 iniset,大于 set-max-intset-entries 用 set。
set-max-intset-entries 512

# 数据量小于等于 zset-max-ziplist-entries 用 ziplist,大于 zset-max-ziplist-entries 用 zset。
zset-max-ziplist-entries 128
#value 大小小于等于 zset-max-ziplist-value 用 ziplist,大于 zset-max-ziplist-value 用 zset。
zset-max-ziplist-value 64

#value 大小小于等于 hll-sparse-max-bytes 使用稀疏数据结构(sparse),大于 hll-sparse-max-bytes 使用稠密的数据结构(dense)。一个比 16000 大的 value 是几乎没用的,建议的 value 大概为 3000。如果对 CPU 要求不高,对空间要求较高的,建议设置到 10000 左右。
hll-sparse-max-bytes 3000

#Redis 将在每 100 毫秒时使用 1 毫秒的 CPU 时间来对 redis 的 hash 表进行重新 hash,可以降低内存的使用。当你的使用场景中,有非常严格的实时性需要,不能够接受 Redis 时不时的对请求有 2 毫秒的延迟的话,把这项配置为 no。如果没有这么严格的实时性要求,可以设置为 yes,以便能够尽可能快的释放内存。
activerehashing yes

## 对客户端输出缓冲进行限制可以强迫那些不从服务器读取数据的客户端断开连接,用来强制关闭传输缓慢的客户端。
# 对于 normal client,第一个 0 表示取消 hard limit,第二个 0 和第三个 0 表示取消 soft limit,normal client 默认取消限制,因为如果没有寻问,他们是不会接收数据的。
client-output-buffer-limit normal 0 0 0
# 对于 slave client 和 MONITER client,如果 client-output-buffer 一旦超过 256mb,又或者超过 64mb 持续 60 秒,那么服务器就会立即断开客户端连接。
client-output-buffer-limit slave 256mb 64mb 60
# 对于 pubsub client,如果 client-output-buffer 一旦超过 32mb,又或者超过 8mb 持续 60 秒,那么服务器就会立即断开客户端连接。
client-output-buffer-limit pubsub 32mb 8mb 60

#redis 执行任务的频率为 1s 除以 hz。
hz 10

# 在 aof 重写的时候,如果打开了 aof-rewrite-incremental-fsync 开关,系统会每 32MB 执行一次 fsync。这对于把文件写入磁盘是有帮助的,可以避免过大的延迟峰值。
aof-rewrite-incremental-fsync yes

自定义脚本

使用 keepalived 做最基本的高可用

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
[root@sg-gop-10-71-12-78 redis-6389]# cat start_redis.sh
/data/running/redis-6389/redis-server /data/running/redis-6389/redis.conf

[root@sg-gop-10-71-12-78 redis-6389]# cat connect_redis.sh
./redis-cli -p 6389

[root@sg-gop-10-71-12-78 redis-6389]# cat stop_redis.sh
kill -9 `cat /var/run/redis_6389.pid`

[root@sg-gop-10-71-12-78 redis-6389]# cat check_redis.sh
#!/bin/bash
# Check if redis is running, return 1 if not.
# Used by keepalived to initiate a failover in case redis is down

REDIS_STATUS=$(telnet 127.0.0.1 6389 < /dev/null | grep "Connected" )
if [ "$REDIS_STATUS" != "" ]
then
exit 0
else
logger "REDIS is NOT running. Setting keepalived state to FAULT."
exit 1
fi

[root@sg-gop-10-71-12-78 redis-6389]# cat change_redis.py
#!/usr/bin/python

import redis
import re
import sys

#main
status=sys.argv[1]
r = redis.StrictRedis(host='localhost', port=6389, db=0)
print r.info().get("role")
if status == 'master':
r.slaveof()
r.config_set("save", "")
print r.info().get("role")

[root@sg-gop-10-71-12-78 redis-6389]# cat keepalived.state.sh
#!/bin/bash

TYPE=$1 #GROUP or INSTANCE
NAME=$2 #name of group or instance
STATE=$3 #MASTER, BACKUP FAULT

case $STATE in
"MASTER") echo $(date)':'$STATE >> /data/running/redis-6389/keep_alived_state #Become redis master
python /data/running/redis-6389/change_redis.py master
exit 0
;;
"BACKUP") echo $(date)':'$STATE >> /data/running/redis-6389/keep_alived_state #Become redis slave
python /data/running/redis-6389/change_redis.py slave
exit 0
;;
"FAULT") echo $(date)':'$STATE >> /data/running/redis-6389/keep_alived_state #restart and become redis slave
exit 0
;;
*) echo "unknown state"
exit 1
;;
esac


[root@sg-gop-10-71-12-77 wangao]# cat /etc/keepalived/keepalived.conf
vrrp_script check_redis {
script "/data/running/redis-6389/check_redis.sh"
interval 2
fall 2
rise 2
}

vrrp_sync_group NC-MAIN-HA {
group {
NC-MAIN-HA-PRI
}
}

vrrp_instance NC-MAIN-HA-PRI {
state BACKUP
interface bond0
virtual_router_id 77
priority 100
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
10.71.13.77/23 dev bond0
}
track_script {
check_redis
}
notify /data/running/redis-6389/keepalived.state.sh
}

前言

企业内部大部分服务器都只有内网,一般可以使用 NAT 方式正向代理访问公网资源。对于 Linux 来说一般通过 ssh 登录服务器,在没有公网 IP 的情况下可以修改 ssh config 配置文件,利用一台可以接入内网并具有公网 IP 的服务器作为代理或者称为 Tunnel 跳板机来管理,可以灵活定制访问规则并优化 ssh 参数让管理更加轻松。

使用 ssh config 作为 ssh 代理轻松管理内网服务器


扩展阅读

SSH CONFIG FILE - https://www.ssh.com/ssh/config/


ssh_config 的配置文件来源

1
2
3
4
5
6
7
8
9
10
11
man ssh_config

NAME
ssh_config -- OpenSSH SSH client configuration files

DESCRIPTION
ssh(1) obtains configuration data from the following sources in the following order:

1. command-line options
2. user's configuration file (~/.ssh/config)
3. system-wide configuration file (/etc/ssh/ssh_config)

ssh 程序可以从三个途径获取配置参数:

  1. 命令行选型,比如 -F configfile
  2. 用户配置文件,放置在 ~/.ssh/config
  3. 系统配置文件,放置在 /etc/ssh/ssh_config(区别于 /etc/ssh/sshd_config)

上面三个途径,前面的途径传入的参数可以覆盖后面的途径传入的参数(与 linux 里的大部分应用类似)。因为 / etc/ssh/ssh_config 会影响 ssh 全局的配置,因此如果想对多主机进行管理(不影响别人的情况下),可以考虑修改自己家目录下的~/.ssh/config 文件(~ 字符表示当前登录用户的家目录)。

ssh_config 配置文件实例

首先看一个配置文件的 demo,假设 8.8.8.8 是你可以直接登录的 Tunnel 公网跳板机,10.65.32.0 是需要管理的内网地址段,前提是 8.8.8.8 和内网服务器的防火墙策略均已配置正确,这里就不再赘述。

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
# cat ~/.ssh/config

StrictHostKeyChecking no
CheckHostIP no

Host 10.65.32.*
HostName %h
ProxyCommand ssh bastion_GOP_SG_NC_MAIN -W %h:%p
Host bastion_GOP_SG_NC_MAIN
HostName 8.8.8.8
port 22
User wangao

Host 10.65.200.*
HostName %h
ProxyCommand ssh bastion_GOP_SG_MH_MAIN -W %h:%p
Host bastion_GOP_SG_MH_MAIN
HostName 9.9.9.9
port 22
User wangao
StrictHostKeyChecking no
CheckHostIP no

Host 10.71.12.*
HostName %h
ProxyCommand ssh bastion_GOP_SG_NC_MAIN -W %h:%p

Host 10.71.13.*
HostName %h
ProxyCommand ssh bastion_GOP_SG_NC_MAIN -W %h:%p

Host 10.71.14.*
HostName %h
ProxyCommand ssh bastion_GOP_SG_NC_MAIN -W %h:%p

Host 10.71.15.*
HostName %h
ProxyCommand ssh bastion_GOP_SG_NC_MAIN -W %h:%p

Host bastion_GOP_SG_NC_MAIN
HostName 8.8.8.8
port 22
User wangao

CheckHostIP no,禁用 known_hosts 检查
Directs ssh to additionally check the host IP address in the known_hosts file.

StrictHostKeyChecking no,跳过 known_hosts 写入
Specifies if ssh should never automatically add host keys to the ~/.ssh/known_hosts file, and refuses to connect to hosts whose host key has changed.

Host 字段
Host 字段配置了登录别名,这里需要注意的是,Host 是支持通配符的, * 代表 0~n 个非空白字符,? 代表一个非空白字符,! 表示例外通配

HostName 字段
指定远程主机名,可以直接使用 IP 地址。如果这个字段中包含 ‘%h’ ,则实际使用时会被命令行中的主机名替换

User 字段
指定登录用户名

IdentityFile 字段
指定密钥认证使用的私钥文件路径。默认为 ~/.ssh/id_rsa。这个字段可以指定多个密钥文件(以 , 分开),在连接的过程中会依次尝试这些密钥文件。和 HostName 字段一样,值也可以直接指定参数代替:

Port 字段
指定远程主机端口号,默认为 22 。

%h,远程主机名
%p,远程端口

参考

如果你还不了解 ssh 或者对 ssh 端口转发感兴趣,可以参考以下内容

SSH 使用密钥登录并禁止口令登录实践 - https://wsgzao.github.io/post/ssh/

玩转 SSH 端口转发 - https://blog.fundebug.com/2017/04/24/ssh-port-forwarding/