docker1-入门

docker组成

docker 分为镜像容器仓库三部分。

  • 仓库:可以说是类似是github一样的东西,github是存放代码的地方,dockerhub是存放docker镜像的地方
  • 镜像:可以认为是我们的源代码,可以根据源代码编译成执行文件(容器)
  • 容器:可以类比是各类软件(软件由代码编译,容器由镜像编译成),由docker镜像生成。

docker安装

docker现在分为ce和ee版本

ce(community-edition)版本是社区办,ee(enterprise-edition)是企业版

社区版 企业版基本 企业版标准 企业版高级
容器引擎,built in orchestration, 网络, 安全 yes yes yes yes
Docker认证的基础架构,插件和ISV容器 yes yes yes
image管理(私有docker registry, caching) Cloud hosted repos yes yes
Docker数据中心(集成容器应用程序管理) yes yes
Docker数据中心(RBAC, LDAP/AD support) yes yes
集成加密管理,镜像签名策略(Integrated secrets mgmt, image signing policy) yes yes
镜像安全扫描(Image security scanning) Preview yes
Support Community Support Business Day or Business Critical Business Day or Business Critical Business Day or Business Critical
免费 750$/年 1500$/年 2000$/年

Ubuntu 安装 Docker CE

1
$ sudo apt-get install docker

启动 Docker CE

1
2
$ sudo systemctl enable docker
$ sudo systemctl start docker

建立 docker 用户组

默认情况下,docker 命令会使用 Unix socket 与 Docker 引擎通讯。而只有 root 用户和 docker 组的用户才可以访问 Docker 引擎的 Unix socket。出于安全考虑,一般 Linux 系统上不会直接使用 root 用户。因此,更好地做法是将需要使用 docker 的用户加入 docker 用户组。

建立 docker 组:

1
$ sudo groupadd docker

将当前用户加入 docker 组:

1
$ sudo usermod -aG docker $USER

镜像加速

鉴于国内网络问题,后续拉取 Docker 镜像十分缓慢,强烈建议安装 Docker 之后配置 国内镜像加速

阿里云镜像仓库

镜像

获取镜像

从 Docker 镜像仓库获取镜像的命令是 docker pull。其命令格式为:

1
docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

具体的选项可以通过 docker pull --help 命令看到。

  • Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub。
  • 仓库名:如之前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。
1
2
3
4
5
6
7
8
9
10
11
sudo docker pull redis
Using default tag: latest
latest: Pulling from library/redis
be8881be8156: Already exists
d6f5ea773ca3: Pull complete
735cc65c0db4: Pull complete
787dddf99946: Pull complete
0733799a7c0a: Pull complete
6d250f04811a: Pull complete
Digest: sha256:858b1677143e9f8455821881115e276f6177221de1c663d0abef9b2fda02d065
Status: Downloaded newer image for redis:latest

如果docker pull没有给出镜像地址,就会默认从docker hub下下载镜像

列出镜像

可以通过docker image来查看本地有哪些镜像

~$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
wordpress latest 3745a1731caf 40 hours ago 408MB
mysql latest 29e0ae3b69b9 2 weeks ago 484MB
redis latest 4e8db158f18d 3 weeks ago 83.4MB
列表包含了 仓库名标签镜像 ID创建时间 以及 所占用的空间

虚悬镜像

有时候有些镜像显示的是<none>。:

1
<none>               <none>              00285df0df87        5 days ago          342 MB

这个镜像原本是有镜像名和标签的,原来为 mongo:3.2,随着官方镜像维护,发布了新版本后,重新 docker pull mongo:3.2 时,mongo:3.2 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 <none>。除了 docker pull 可能导致这种情况,docker build 也同样可以导致这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none> 的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image) ,可以用下面的命令专门显示这类镜像:

1
2
3
$ docker images -f dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB

一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除。

1
$ docker image prune

中间层镜像

为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 docker image ls 列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 -a 参数。

1
$ docker image ls -a

这样会看到很多无标签的镜像,与之前的虚悬镜像不同,这些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不应该删除,否则会导致上层镜像因为依赖丢失而出错。实际上,这些镜像也没必要删除,因为之前说过,相同的层只会存一遍,而这些镜像是别的镜像的依赖,因此并不会因为它们被列出来而多存了一份,无论如何你也会需要它们。只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除。

删除本地镜像

如果要删除本地的镜像,可以使用 docker image rm 命令或者docker rmi,其格式为:

1
$ docker image rm [选项] <镜像1> [<镜像2> ...]
1
$ docker rmi [选项] <镜像1> [<镜像2> ...]

用 ID、镜像名、摘要删除镜像

其中,<镜像> 可以是 镜像短 ID镜像长 ID镜像名 或者 镜像摘要

sudo docker rmi 3745a1731caf

1
2
3
4
Untagged: centos:latest
Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38

容器

运行容器

有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器。

docker run --name redis_test -p 6379:6379 -d redis

~$ sudo docker run –name redis_test -p 6379:6379 -d redis
5d0fde30bedc6192e77b75bbb8a5a9894b593f7eb58411f0b96a2270872445da

~$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5d0fde30bedc redis “docker-entrypoint.s…” 45 seconds ago Up 38 seconds 0.0.0.0:6379->6379/tcp redis_test

docker run就是运行容器的命令,运行他的时候可以带一些参数,详细可以通过docke run --help

  • --rm:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm
  • redis:这是指用 redis 镜像为基础来启动容器。
  • --name:为容器指定名称。
  • -p:将容器的端口发布到主机.这里就是讲reids容器的6379端口映射到主机上的6379端口(ip:hostPort:containerPort)
  • -d:在后台运行容器并打印容器ID

进入容器内部

进入redis容器内部,运行redis-cli,来操作redis:

docker exec -it containerid /bin/bash

  • exec:在正在运行的容器中运行命令

  • -it:这是两个参数,一个是 -i:交互式操作,保证容器中stdin(一般指键盘输入到缓冲区里的东西)是开放的,一个是 -t :告诉docker为要创建的容器分配一个伪tty终端。这样,新创建的容器才能够提供一个交互式shell

  • bash:放在容器id后的是命令,这里我们希望有个交互式 Shell,因此用的是 bash

~$ sudo docker exec -it 5d0fde30bedc /bin/bash
root@5d0fde30bedc:/data# redis-cli
127.0.0.1:6379> set test value
OK
127.0.0.1:6379> get test
“value”
127.0.0.1:6379>

想要退出就按ctrl+d

停止,删除容器

  1. 停止容器运行docker stop containerid
  2. 删除容器docker rm containerid 删除容器必须要先将容器停止之后.
  3. 删除所有容器: docker rm `sudo docker ps -a -q`

启动,重启容器

  1. 启动已经停止运行的容器docker start containername或者通过id启动docker start containerid

  2. 重启容器:docker restart containername或者docker restart containerid

  3. 自动重启容器

    如果容器因为某种错误而导致了容器停止运行,还可以通过--restart标志,让docker自动重新启动该容器。--restart标志会检查容器的退出代码,并根据此来决定是否要重启容器。默认的行为是docker不会容器容器。

    –restart:参数:

    always:无论容器的退出代码是什么,docker 都会自动重启该容器,

    on-failure:只有当容器退出的代码是非0值的时候,才会自动重启,on-failure可以接受一个可选的重启次数参数,如--restart=on-failure:5.这样,当容器退出代码为非0的时候,docker会尝试自动重启该容器,最多重启5此

TCP-SYN

简介

传输控制协议中的TCP三次握手(也称为TCP握手;三次消息握手或SYN-SYN-ACK)是TCP在基于互联网协议的网络上建立TCP / IP连接所使用的方法。 TCP的三路握手技术通常被称为“SYN-SYN-ACK”(或者更准确地说是SYN,SYN-ACK,ACK),因为TCP传输三条消息来协商和启动两台计算机之间的TCP会话。 TCP握手机制被设计为使得两个尝试通信的计算机可以在传输诸如SSH和HTTP web浏览器请求之类的数据之前协商网络TCP套接字连接的参数。

此三次握手过程的设计也是为了使两端可以同时启动和协商单独的TCP套接字连接。能够同时在两个方向上协商多个TCP套接字连接允许多路复用单个物理网络接口(例如以太网)以同时传输多个TCP数据流。

tcp帧格式

tcp数据包的格式如下:

源端口号和目的端口号与udp中类似,用于寻找发端和收端应用进程

  源端口号(Source Port)和目的端口号(Destination port)与udp中类似,用于寻找发端和收端应用进程。这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接,在网络编程中,一般一个IP地址和一个端口号组合称为一个套接字(socket)。 (源端口和目的端口各占2个字节,总共四个字节)
  序列号(SequenceNumber):32位的序列号标识了TCP报文中第一个byte在对应方向的传输中对应的字节序号。当SYN出现,SN=ISN(随机值)单位是byte。比如发送端发送的一个TCP包净荷(不包含TCP头20byte)为12byte,SN为5,则发送端接着发送的下一个数据包的时候,SN应该设置为5+12=17。通过序列号,TCP接收端可以识别出重复接收到的TCP包,从而丢弃重复包,同时对于乱序数据包也可以依靠系列号进行重排序,进而对高层提供有序的数据流。另外如果接收的包中包含SYN或FIN标志位,逻辑上也占用1个byte,应答号需加1。 (四字节)
  确认序号(Acknowledgment Number简称ACK Number):32位的ACK Number标识了报文发送端期望接收的字节序列。如果设置了ACK控制位,这个值表示一个准备接收的包的序列码,注意是准备接收的包,比如当前接收端接收到一个净荷为12byte的数据包,SN为5,则会回复一个确认收到的数据包,如果这个数据包之前的数据也都已经收到了,这个数据包中的ACK Number则设置为12+5=17,表示之前的数据都已经收到了,准备接受SN=17的数据包。 (四字节)  

头长(Header Length):4位包括TCP头大小,指示TCP头的长度,即数据从何处开始。

CWR(Congestion Window Reduce):拥塞窗口减少标志set by sender,用来表明它接收到了设置ECE标志的TCP包。并且sender 在收到消息之后已经通过降低发送窗口的大小来降低发送速率。

ECE(ECN Echo):ECN响应标志被用来在TCP3次握手时表明一个TCP端是具备ECN功能的。在数据传输过程中也用来表明接收到的TCP包的IP头部的ECN被设置为11。注:IP头部的ECN被设置为11表明网络线路拥堵。

  URG: 紧急指针( urgent pointer)有效。 (1bit)
  ACK: 取值1代表Acknowledgment Number字段有效,这是一个确认的TCP包,取值0则不是确认包。后续文章介绍中当ACK标志位有效的时候我们称呼这个包为ACK包,使用大写的ACK称呼。
  PSH: 该标志置位时,一般是表示发送端缓存中已经没有待发送的数据,接收端不将该数据进行队列处理,而是尽可能快将数据转由应用处理。在处理 telnet 或 rlogin 等交互模式的连接时,该标志总是置位的。
  RST: 重建连接。用于reset相应的TCP连接。通常在发生异常或者错误的时候会触发复位TCP连接。
  SYN: 同步序列编号(Synchronize Sequence Numbers)有效。该标志仅在三次握手建立TCP连接时有效。)
  FIN: 发端完成发送任务。 No more data from sender。当FIN标志有效的时候我们称呼这个包为FIN包。
  窗口大小:16位,该值指示了从Ack Number开始还愿意接收多少byte的数据量,也即用来表示当前接收端的接收窗还有多少剩余空间,用于TCP的流量控制。
  检验和:检16位TCP头。发送端基于数据内容计算一个数值,接收端要与发送端数值结果完全一样,才能证明数据的有效性。接收端checksum校验失败的时候会直接丢掉这个数据包。CheckSum是根据伪头+TCP头+TCP数据三部分进行计算的。

优先指针(紧急,Urgent Pointer):16位,指向后面是优先数据的字节,在URG标志设置了时才有效。如果URG标志没有被设置,紧急域作为填充。

选项(Option):长度不定,但长度必须以是32bits的整数倍。常见的选项包括MSS、SACK、Timestamp等等。

tcp报文段

ip数据报

protobuf

简介

由于线上项目在做压力测试的时候,发现在数据量不是特别大的时候cpu资源消耗特别多,经过go pprof工具的cpu性能分析发现,因为在此项目中有大量使用gob序列化和发序列化的地方导致,因为gob序列化需要用到反射,性能很差,最后在做了各个序列化对比后,发现google的Protocol Buffers序列化与反序列化性能消耗差不多,性能是gob的十几倍。因此想将线上数据改造为protobuf格式。

Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

安装 Google Protocol Buffer

  • protoc可执行文件下载地址:https://github.com/google/protobuf/releases

  • 安装编译器插件protoc-gen-go (protoc-gen-go用于生成Go语言代码)

    1
    go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

编写.proto文件

首先我们需要编写一个 proto 文件,定义我们程序中需要处理的结构化数据,在 protobuf 的术语中,结构化数据被称为 Message。proto 文件非常类似 java 或者 C 语言的数据定义。代码清单 1 显示了例子应用中的 proto 文件内容。

清单 1. proto 文件
1
2
3
4
5
6
7
8
syntax = "proto3";
package lm;
message helloworld
{
required int32 id = 1; // ID
required string str = 2; // str
optional int32 opt = 3; //optional field
}

一个比较好的习惯是认真对待 proto 文件的文件名。比如将命名规则定于如下:

packageName.MessageName.proto

在上例中,package 名字叫做 lm,定义了一个消息 helloworld,该消息有三个成员,类型为 int32 的 id,另一个为类型为 string 的成员 str。opt 是一个可选的成员,即消息中可以不包含该成员。

编译 .proto 文件

写好 proto 文件之后就可以用 Protobuf 编译器将该文件编译成目标语言了。本例中我们将使用 Go。

这里详细介绍golang的编译姿势:

  • -I 参数:指定import路径,可以指定多个-I参数,编译时按顺序查找,不指定时默认查找当前目录
  • --go_out :golang编译支持,支持以下参数
    • plugins=plugin1+plugin2 - 指定插件,目前只支持grpc,即:plugins=grpc
    • M 参数 - 指定导入的.proto文件路径编译后对应的golang包名(不指定本参数默认就是.proto文件中import语句的路径)
    • import_prefix=xxx - 为所有import路径添加前缀,主要用于编译子目录内的多个proto文件,这个参数按理说很有用,尤其适用替代一些情况时的M参数,但是实际使用时有个蛋疼的问题导致并不能达到我们预想的效果,自己尝试看看吧
    • import_path=foo/bar - 用于指定未声明packagego_package的文件的包名,最右面的斜线前的字符会被忽略
    • 末尾 :编译文件路径 .proto文件路径(支持通配符)

完整示例:

假设您的 proto 文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一个目录下,则可以使用如下命令:

protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto

命令将生成一个文件:

addressbook.pb.go

redis-eval

EVAL

EVAL script numkeys key [key …] arg [arg …]

从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行求值。

  • script 参数是一段 Lua 5.1 脚本程序,它会被运行在 Redis 服务器上下文中,这段脚本不必(也不应该)定义为一个 Lua 函数。
  • numkeys 参数用于指定键名参数的个数。
  • 键名参数 key [key ...]EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1]KEYS[2] ,以此类推)。
  • 在命令的最后,那些不是键名参数的附加参数 arg [arg ...] ,可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1]ARGV[2] ,诸如此类)。
1
2
3
4
5
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

通过docker 安装reids

1
docker run --name redis-lua -p 6379:6379 -d redis

~ docker ps

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
939cd668f9d0 redis “docker-entrypoint.s…” 6 seconds ago Up 2 seconds 0.0.0.0:6379->6379/tcp redis-lua

执行以下命令进入docker容器内部:

1
docker exec -it 939cd668f9d0 /bin/bash

执行eval命令

在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是:

  • redis.call()
  • redis.pcall()

这两个函数的唯一区别在于它们使用不同的方式处理执行命令所产生的错误。

redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因:

1
2
3
4
5
6
> redis> lpush foo a
> (integer) 1
>
> redis> eval "return redis.call('get', 'foo')" 0
> (error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value
>

redis.call() 不同, redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误:

1
2
3
> redis 127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0
> (error) ERR Operation against a key holding the wrong kind of value
>

redis.call()redis.pcall() 两个函数的参数可以是任何格式良好(well formed)的 Redis 命令:

1
2
> eval "return redis.call('set','foo','bar')" 0
OK

需要注意的是,上面这段脚本的确实现了将键 foo 的值设为 bar 的目的,但是,它违反了 EVAL 命令的语义,因为脚本里使用的所有键都应该由 KEYS 数组来传递,就像这样:

1
2
> eval "return redis.call('set',KEYS[1],'bar')" 1 foo
OK

golang中使用Lua脚本操作Redis

为了在我的一个基本库中降低与Redis的通讯成本,我将一系列操作封装到LUA脚本中,借助Redis提供的EVAL命令来简化操作。
EVAL能够提供的特性:

  1. 可以在LUA脚本中封装若干操作,如果有多条Redis指令,封装好之后只需向Redis一次性发送所有参数即可获得结果
  2. Redis可以保证Lua脚本运行期间不会有其他命令插入执行,提供像数据库事务一样的原子性
  3. Redis会根据脚本的SHA值缓存脚本,已经缓存过的脚本不需要再次传输Lua代码,减少了通信成本,此外在自己代码中改变Lua脚本,执行时Redis必定也会使用最新的代码。

导入常见的Go库如 “github.com/go-redis/redis”,就可以实现以下代码。

生成一段Lua脚本

1
2
3
4
5
6
7
8
// KEYS: key for record
// ARGV: fieldName, currentUnixTimestamp, recordTTL
// Update expire field of record key to current timestamp, and renew key expiration
var updateRecordExpireScript = redis.NewScript(`
redis.call("EXPIRE", KEYS[1], ARGV[3])
redis.call("HSET", KEYS[1], ARGV[1], ARGV[2])
return 1
`)

该变量创建时,Lua代码不会被执行,也不需要有已存的Redis连接。 Redis提供的Lua脚本支持,默认有KEYS、ARGV两个数组,KEYS代表脚本运行时传入的若干键值,ARGV代表传入的若干参数。由于Lua代码需要保持简洁,难免难以读懂,最好为这些参数写一些注释

注意:上面一段代码使用跨行,`所在的行虽然空白回车,也会被认为是一行,报错时不要看错代码行号。

运行一段Lua脚本

1
2
3
updateRecordExpireScript.Run(c.Client, []string{recordKey(key)}, 
expireField,
time.Now().UTC().UnixNano(), int64(c.opt.RecordTTL/time.Second)).Err()

运行时,Run将会先通过EVALSHA尝试通过缓存运行脚本。如果没有缓存,则使用EVAL运行,这时Lua脚本才会被整个传入Redis。

Lua脚本的限制

  1. Redis不提供引入额外的包,例如os等,只有redis这一个包可用。
  2. Lua脚本将会在一个函数中运行,所有变量必须使用local声明
  3. return返回多个值时,Redis将会只给你第一个

脚本中的类型限制

  1. 脚本返回nil时,Go中得到的是err = redis.Nil(与Get找不到值相同)
  2. 脚本返回false时,Go中得到的是nil,脚本返回true时,Go中得到的是int64类型的1
  3. 脚本返回{“ok”: …}时,Go中得到的是redis的status类型(true/false)
  4. 脚本返回{“err”: …}时,Go中得到的是err值,也可以通过return redis.error_reply(“My Error”)达成
  5. 脚本返回number类型时,Go中得到的是int64类型
  6. 传入脚本的KEYS/ARGV中的值一律为string类型,要转换为数字类型应当使用to_number

    如果脚本运行了很久会发生什么?

Lua脚本运行期间,为了避免被其他操作污染数据,这期间将不能执行其它命令,一直等到执行完毕才可以继续执行其它请求。当Lua脚本执行时间超过了lua-time-limit时,其他请求将会收到Busy错误,除非这些请求是SCRIPT KILL(杀掉脚本)或者SHUTDOWN NOSAVE(不保存结果直接关闭Redis)

参考:

Go中通过Lua脚本操作Redis

redis命令参考

lua-1

简介

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。。Lua是类C的,所以,他是大小写字符敏感的。

Lua 环境安装

Linux 系统上安装

Linux & Mac上安装 Lua 安装非常简单,只需要下载源码包并在终端解压编译即可。

1
2
3
4
5
wget https://www.lua.org/ftp/lua-5.3.5.tar.gz
tar zxf lua-5.3.5.tar.gz
cd lua-5.3.5/
make linux test
sudo make install

编写Lua脚本的helloworld

Lua脚本的语句的分号是可选的,这个和GO语言很类似。

  • 在hello.lua中编写lua代码
1
2
3
vim hello.lua
print("Hello World")
lua hello.lua
  • 我们也可以将代码修改为如下形式来执行脚本(在开头添加:#!/usr/local/bin/lua):
1
2
3
4
#!/usr/local/bin/lua

print("Hello World!")
print("www.runoob.com")
1
2
chmod 766 hell.lua
./hello.lua
  • 你可以像python一样,在命令行上运行lua命令后进入lua的shell中执行语句。

Lua 交互式编程模式可以通过命令 lua -i 或 lua 来启用:

~ lua
Lua 5.3.5 Copyright (C) 1994-2018 Lua.org, PUC-Rio

>print(“hello world”)

hello world

Lua 基本语法

注释

单行注释

两个减号是单行注释:

1
--

多行注释

1
2
3
4
--[[
多行注释
多行注释
--]]

标示符

Lua 标示符用于定义一个变量,函数获取其他用户定义的项。标示符以一个字母 A 到 Z 或 a 到 z 或下划线 _ 开头后加上0个或多个字母,下划线,数字(0到9)。

关键词

以下列出了 Lua 的保留关键字。保留关键字不能作为常量或变量或其他用户自定义标示符:

and break do else
elseif end false for
function if in local
nil not or repeat
return then true until
while

一般约定,以下划线开头连接一串大写字母的名字(比如 _VERSION)被保留用于 Lua 内部全局变量。

Lua 变量

Lua 变量有三种类型:全局变量、局部变量、表中的域。

全局变量

需要注意的是:lua中的变量如果没有特殊说明,全是全局变量,那怕是语句块或是函数里。变量前加local关键字的是局部变量。

全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是:nil。

1
2
3
4
5
6
> print(b)
nil
> b=10
> print(b)
10
>

如果你想删除一个全局变量,只需要将变量赋值为nil。

1
2
b = nil
print(b) --> nil

这样变量b就好像从没被使用过一样。换句话说, 当且仅当一个变量不等于nil时,这个变量即存在。

控制语句

while循环
1
2
3
4
5
6
7
sum = 0
num = 1
while num <= 100 do
sum = sum + num
num = num + 1
end
print("sum =",sum)
if-else分支
1
2
3
4
5
6
7
8
9
10
if age == 40 and sex =="Male" then
print("男人四十一枝花")
elseif age > 60 and sex ~="Female" then
print("old man without country!")
elseif age < 20 then
io.write("too young, too naive!\n")
else
local age = io.read()
print("Your age is "..age)
end

上面的语句不但展示了if-else语句,也展示了
1)“~=”是不等于,而不是!=
2)io库的分别从stdin和stdout读写的read和write函数
3)字符串的拼接操作符“..”

(注意:Lua没有++或是+=这样的操作)

条件表达式中的与或非为分是:and, or, not关键字。

for 循环

从1加到100

1
2
3
4
sum = 0
for i = 1, 100 do
sum = sum + i
end

从1到100的奇数和

1
2
3
4
sum = 0
for i=1,100,2 do
sum = sum +i
end

从100到1的偶数和

1
2
3
4
sum = 0
for i = 100, 1, -2 do
sum = sum + i
end
until循环
1
2
3
4
5
sum = 2
repeat
sum = sum ^ 2 --幂操作
print(sum)
until sum >1000

函数

Lua的函数和Javascript的很像

递归

1
2
3
4
function fib(n)
if n < 2 then return 1 end
return fib(n - 2) + fib(n - 1)
end

闭包

1
2
3
4
5
6
7
8
9
10
11
function newCounter()
local i = 0
return function() -- anonymous function
i = i + 1
return i
end
end

c1 = newCounter()
print(c1()) --> 1
print(c1()) --> 2

mysql深入

Replace into

Replace into是Insert into的增强版。在向表中插入数据时,我们经常会遇到这样的情况:1、首先判断数据是否存在;2、如果不存在,则插入;3、如果存在,则更新。

但是Replace也有坑,

删除记录并且数据会变化

  1. Replace into的执行步骤其实就是,先去检查查询当前要插入的数据的唯一索引是否存在,如果存在,则将当前的数据删除,然后重新新增一条数据,新增的数据除了自己设置的值,其他的值都是,默认值,假设test表有三个字段

id key value,id为自增主键,key为唯一索引,

1 1 1

2 2 2

3 3 3

那么replace into test (key,value) values (3,4);的时候,则会去删除当前的记key=3的记录,并重新insert一条新的记录,此时表面看是将数据更新了,但是,自增id的值已经变了。

id key value

1 1 1

2 2 2

4 3 4

删除多条记录

在表中有超过一个的唯一索引。在这种情况下,REPLACE将考虑每一个唯一索引,并对每一个索引对应的重复记录都删除,然后插入这条新记录。

假设有一个table1表,有3个字段a, b, c。它们都有一个唯一索引。

假设table1中已经有了3条记录   

a b c   

1 1 1   

2 2 2   

3 3 3

下面我们使用REPLACE语句向table1中插入一条记录。   

REPLACE INTO table1(a, b, c) VALUES(1,2,3);   

返回的结果如下   Query OK, 4 rows affected (0.00 sec)   

在table1中的记录如下  

 a b c   

1 2 3

我们可以看到,REPLACE将原先的3条记录都删除了,然后将(1, 2, 3)插入。

替代方法

  1. 通过两条SQL语句
1
2
3
4
5
SELECT COUNT(*) FROM xxx WHERE ID=xxx;
if (x == 0)
INSERT INTO xxx VALUES;
else
UPDATE xxx SET ;
  1. 如果在INSERT语句末尾指定了ON DUPLICATE KEY UPDATE,并且插入行后会导致在一个UNIQUE索引或PRIMARY KEY中出现重复值,则在出现重复值的行执行UPDATE;如果不会导致唯一值列重复的问题,则插入新行。不过( 这语法效率超低。此语发是mysql独有)

    1
    2
    INSERT INTO TABLE (a,c) VALUES (1,3) ON DUPLICATE KEY UPDATE c=c+1;
    UPDATE TABLE SET c=c+1 WHERE a=1;

详细可以参考:

mysql的replace into“坑”

Linux下性能分析工具

linux下性能分析工具还有很多,在平常检测系统性能的时候主要关注的无非是cpu,内存,磁盘io,网络带宽等。

top命令

top命令工可以查看的状态就比较多了,他可以查看cpu,内存,平均负载,磁盘io等状况,使用也很简单,直接在命令行输入top。但是top对资源占用比较高,少用

top的 -b 选项开启批处理模式,将每次刷新全部打印到stdout

top的 -n 选项指定退出top命令前刷新多少次信息。

top命令的输出:

第1行:主要关注最后三列就是系统的平均负载:
即系统1分钟、5分钟、15分钟内的平均负载,判断一个系统负载是否偏高需要计算单核CPU的平均负载,这里显示的系统平均负载 / CPU核数,一般以0.7为比较合适的值。偏高说明有比较多的进程在等待使用CPU资源。
如果1分钟平均负载很高,而15分钟平均负载很低,说明服务器正在命令高负载情况,需要进一步排查CPU资源都消耗在了哪里。反之,如果15分钟平均负载很高,1分钟平均负载较低,则有可能是CPU资源紧张时刻已经过去。

其他查看平均负载的命令还有

  • tload: 能够绘制出负载变化的图形
  • uptime:显示平均负载的同时,还显示开机以来的时间,和top的第一行一样,如果想要持续观察平均负载,可以使用watch uptime,默认刷新时间是两秒
  • w: 显示uptime的信息以外,还同时显示已登录的用户

第3行:当前的CPU运行情况:

  • us:非nice用户进程占用CPU的比率

  • sy:内核、内核进程占用CPU的比率;

  • ni:如果一些用户进程修改过优先级,这里显示这些进程占用CPU时间的比率;

  • id:CPU空闲比率,如果系统缓慢而这个值很高,说明系统慢的原因不是CPU负载高;

  • wa:CPU等待执行I/O操作的时间比率,该指标可以用来排查磁盘I/O的问题,通常结合wa和id判断

  • hi:CPU处理硬件终端所占时间的比率;

  • si:CPU处理软件终端所占时间的比率;

  • st:流逝的时间,虚拟机中的其他任务所占CPU时间的比率;

  用户进程占比高,wa低,说明系统缓慢的原因在于进程占用大量CPU,通常还会伴有教低的id,说明CPU空转时间很少;

  wa低,id高,可以排除CPU资源瓶颈的可能。  

  wa高,说明I/O占用了大量的CPU时间,需要检查交换空间的使用,交换空间位于磁盘上,性能远低于内存,当内存耗尽开始使用交换空间时,将会给性能带来严重影响,所以对于性能要求较高的服务器,一般建议关闭交换空间。另一方面,如果内存充足,但wa很高,说明需要检查哪个进程占用了大量的I/O资源。

mpstat

该命令可以显示每个CPU的占用情况,如果有一个CPU占用率特别高,那么有可能是一个单线程应用程序引起的。

mpstat -P ALL 1

1
2
3
4
5
mpstat [-P {|ALL}] [internal [count]]
-P {|ALL} 表示监控哪个CPU, cpu在[0,cpu个数-1]中取值
internal 相邻的两次采样的间隔时间、
count 采样的次数,count只能和delay一起使用
当没有参数时,mpstat则显示系统启动以后所有信息的平均值。有interval时,第一行的信息自系统启动以来的平均信息。从第二行开始,输出为前一个interval时间段的平均信息。

字段的含义如下:

1
2
3
4
5
6
7
%user      在internal时间段里,用户态的CPU时间(%),不包含nice值为负进程  (usr/total)*100
%nice 在internal时间段里,nice值为负进程的CPU时间(%) (nice/total)*100
%sys 在internal时间段里,内核时间(%) (system/total)*100
%iowait 在internal时间段里,硬盘IO等待时间(%) (iowait/total)*100
%irq 在internal时间段里,硬中断时间(%) (irq/total)*100
%soft 在internal时间段里,软中断时间(%) (softirq/total)*100
%idle 在internal时间段里,CPU除去等待磁盘IO操作外的因为任何原因而空闲的时间闲置时间(%) (idle/total)*100

计算公式如下

1
2
3
4
5
total_cur=user+system+nice+idle+iowait+irq+softirq
total_pre=pre_user+ pre_system+ pre_nice+ pre_idle+ pre_iowait+ pre_irq+ pre_softirq
user=user_cur – user_pre
total=total_cur-total_pre
其中_cur 表示当前值,_pre表示interval时间前的值。上表中的所有值可取到两位小数点。

goroutine scheduler

一、Goroutine调度器

goroutine是golang内置的协程,当我需要并发执行一些任务的时候,在go语言中可以使用go关键字来创建goroutine。

1
2
3
go func(){
// dosth
}()

相比于c++和java等语言创建线程,go语言创建的goroutine是在进程和线程的基础上做更高层次的抽象,Go采用了用户层轻量级thread或者说是类coroutine的概念来解决这些问题,Go将之称为”goroutine“。goroutine占用的资源非常小(Go 1.4将每个goroutine stack的size默认设置为2k),goroutine调度的切换也不用陷入(trap)操作系统内核层完成,代价很低。因此,一个Go程序中可以创建成千上万个并发的goroutine。所有的Go代码都在goroutine中执行,哪怕是go的runtime也不例外。将这些goroutines按照一定算法放到“CPU”上执行的程序就称为goroutine调度器goroutine scheduler

但是针对操作系统层面,操作系统是不知道goroutine的存在的,goroutine的调度全部是靠自己内部完成的,实现Go程序内goroutine之间“公平”的竞争“CPU”资源,这个任务就落到了Go runtime头上,要知道在一个Go程序中,除了用户代码,剩下的就是go runtime了。

二、Go调度器模型与演化过程

goroutine是通过三种基本对象互相协作(GMP),来实现在用户空间管理和调度并发任务。

基本关系是

来自go语言学习笔记

此图来自雨痕的go语言学习笔记

  1. 首先是Processor(简称P):

    ​ 他的作用类似于CPU核,用来控制可同时并发执行的任务数,每个工作线程都必须绑定一个有效P才被允许执行任务。否则只能休眠。直到有空闲P时被唤醒,P还为线程提供执行资源,比如内存分配本地任务队列等。线程独享所绑定的P资源,可以在无锁状态下执行高效操作。

  2. 其次是Goroutine(简称G):

    ​ 进程内一切都在以goroutine方式运行,包括运行时相关的服务,以及mani.main入口函数。需要指出,G并非执行体,他仅仅保存并发任务状态,为任务执行提供所需的栈内存空间。G任务创建后被放置在P本地队列火全局队列,等待工作线程调度执行

  3. 最后是系统线程machine(简称M):

    ​ 实际执行体是系统线程和p绑定,以调度循环方式不停执行G并发任务。M通过修改寄存器,将执行栈指向G自带的栈内存,并在此空间内分配堆栈帧,执行任务函数。当需要中途切换的时候,只要将相关寄存器值保存回G空间即可维持状态,任何M都可以据此恢复执行。线程仅负责执行。不在持有状态,这是并发任务跨线程调度,实现多路复用的根本所在。

虽然P/M构成执行组合体,但两者数量不是一一对应的。通常情况下,p的数量相对恒定,默认是cpu的核心数。但是也可以更多或者更少,可以通过runtime.GOMAXPROCS()函数来设置。但是M则是由调度器按需创建的,举例来说,当M因陷入系统调用而长时间阻塞时,p会被健康线程抢回去,去新建(或唤醒)一个去执行其他任务,这样M的数量就会增长。

优点:

​ 因为G初始栈仅有2KB,且创建操作只是在用户空间简单的分配对象,远比要进入内核态分配线程要简单的多。

调度器让多个M进入调度循环,不停获取并执行任务,这样就可以创建成千上万个并发任务。

疑问:

​ 其实按照上面的描述,goroutine调度队列只需要有M和g就可以了,用户呢创建goroutine,go运行时机制去创建线程来调度goroutine就可以,为什么要增加一个p来作为中间层呢?

解析:

​ 查了资料之后,发现原来Go 1.0正式发布的时候确实是实现的G-M模型,并没有P的存在,但是此模型存在一系列不足,前Intel blackbelt工程师、现Google工程师Dmitry Vyukov在其《Scalable Go Scheduler Design》一文中指出了G-M模型的一个重要不足: 限制了Go并发程序的伸缩性,尤其是对那些有高吞吐或并行计算需求的服务程序。主要体现在如下几个方面:

  1. 单一全局互斥锁(Sched.Lock)和集中状态存储的存在导致所有goroutine相关操作,比如:创建、重新调度等都要上锁;
  2. goroutine传递问题:M经常在M之间传递”可运行”的goroutine,这导致调度延迟增大以及额外的性能损耗;
  3. 每个M做内存缓存,导致内存占用过高,数据局部性较差;
  4. 由于syscall调用而形成的剧烈的worker thread阻塞和解除阻塞,导致额外的性能损耗

于是Dmitry Vyukov亲自操刀改进Go scheduler,在Go 1.1中实现了G-P-M调度模型work stealing算法,这个模型一直沿用至今

  • G: 表示goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等;另外G对象是可以重用的。
  • P: 表示逻辑processor,P的数量决定了系统内最大可并行的G的数量(前提:系统的物理cpu核数>=P的数量);P的最大作用还是其拥有的各种G对象队列、链表、一些cache和状态。
  • M: M代表着真正的执行计算资源。在绑定有效的p后,进入schedule循环;而schedule循环的机制大致是从各种队列、p的本地队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到m,如此反复。M并不保留G状态,这是G可以跨M调度的基础。

preview

  • 当一个OS线程M0陷入阻塞时(一般是channel阻塞或network I/O阻塞或者system call阻塞),P转而在OS线程M1上运行。调度器保证有足够的线程来运行所以的context P。图中的M1可能是被创建,或者从线程缓存中取出。当M0返回时,它必须尝试取得一个context P来运行goroutine,一般情况下,它会从其他的OS线程那里steal偷一个context P过来,如果没有偷到的话,它就把goroutine放在一个global runqueue里,然后自己就去睡大觉了(放入线程缓存里)。Contexts P们也会周期性的检查global runqueue,否则global runqueue上的goroutine永远无法执行

  • 另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了一个上下文P闲着没事儿干而系统却任然忙碌。但是如果global runqueue没有任务G了,那么P就不得不从其他的上下文P那里拿一些G来执行。一般来说,如果上下文P从其他的上下文P那里要偷一个任务的话,一般就‘偷’run queue的一半,这就确保了每个OS线程都能充分的使用。

三、调度器状态的查看方法

Go提供了调度器当前状态的查看方法:使用Go运行时环境变量GODEBUG。

GODEBUG这个Go运行时环境变量很是强大,通过给其传入不同的key1=value1,key2=value2… 组合,Go的runtime会输出不同的调试信息,比如在这里我们给GODEBUG传入了”schedtrace=1000″,其含义就是每1000ms,打印输出一次goroutine scheduler的状态,每次一行。每一行各字段含义如下:

SCHED 6016ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=1 [3 4 0 10]

SCHED:调试信息输出标志字符串,代表本行是goroutine scheduler的输出;

6016ms:即从程序启动到输出这行日志的时间;

gomaxprocs: P的数量;

idleprocs: 处于idle状态的P的数量;通过gomaxprocs和idleprocs的差值,我们就可知道执行go代码的P的数量;

threads: os threads的数量,包含scheduler使用的m数量,加上runtime自用的类似sysmon这样的thread的数量;

spinningthreads: 处于自旋状态的os thread数量;

idlethread: 处于idle状态的os thread的数量;

runqueue=1: go scheduler全局队列中G的数量;

[3 4 0 10]: 分别为4个P的local queue中的G的数量。

参考

也谈goroutine调度器

Go语言学习笔记
Golang 的 goroutine 是如何实现的?

Go里面的堆栈跟踪

转载翻译,原文地址:Stack Traces In Go

Go里面的堆栈跟踪

在Go语言中有一些调试技巧能帮助我们快速找到问题,有时候你想尽可能多的记录异常但仍觉得不够,搞清楚堆栈的意义有助于定位Bug或者记录更完整的信息。

本文将讨论堆栈跟踪信息以及如何在堆栈中识别函数所传递的参数。

Functions (函数的情况)

先从这段代码开始:

清单1

1
2
3
4
5
6
7
8
9
10
package main

func main() {
slice := make([]string, 2, 4)
Example(slice, "hello", 10)
}

func Example(slice []string, str string, i int) {
panic("Want stack trace")
}

清单1显示了一个程序,其中main函数在第05行调用Example函数.Example函数在第08行声明并接受三个参数,1个string类型的slice, 1个string和1个integer, 。 Example执行的唯一代码是调用第09行的内置函数panic,它会立即生成堆栈跟踪:

清单2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Panic: Want stack trace

goroutine 1 [running]:
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:9 +0x64
main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:5 +0x85

goroutine 2 [runnable]:
runtime.forcegchelper()
/Users/bill/go/src/runtime/proc.go:90
runtime.goexit()
/Users/bill/go/src/runtime/asm_amd64.s:2232 +0x1

goroutine 3 [runnable]:
runtime.bgsweep()
/Users/bill/go/src/runtime/mgc0.go:82
runtime.goexit()
/Users/bill/go/src/runtime/asm_amd64.s:2232 +0x1

清单2中的堆栈跟踪显示了panic是存在的所有goroutine,每个程序的状态以及相应goroutine下的调用堆栈。

正在运行的goroutine和导致堆栈跟踪的goroutine将位于顶部。让我们关注报了panic的goroutine.

清单3

1
2
3
4
5
6
7
01 goroutine 1 [running]:
02 main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:9 +0x64
03 main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:5 +0x85

清单三中地 01 行的的堆栈跟踪显示goroutine 1 在panic之前运行,在第 02 行,我们看到panic的代码在package main中的Example函数中。缩进的行显示了次函数所在的代码文件和路径以及正在执行的代码行。在这种情况下,第 09 行的代码正在运行,这是对panic的调用。

第 03 行显示调用Example的函数的名称,这是main包中的主要功能,在函数名称下面,缩进的行显示了对Example进行调用的代码文件的路径和代码行

堆栈工资显示goroutine范围内的函数调用链,直到发生panic发生,现在让我们关注传递给Example函数的每个参数的值:

清单4

1
2
3
4
5
6
7
8
9
// Declaration
main.Example(slice []string, str string, i int)

// Call to Example by main.
slice := make([]string, 2, 4)
Example(slice, "hello", 10)

// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

清单4 这里展示了在main中带参数调用Example函数时的堆栈信息 。 将堆栈跟踪中的值与函数声明进行比较时,它似乎不匹配。 Example函数的声明接受三个参数,但堆栈跟踪显示六个十六进制值。 要理解值如何与参数匹配的关键需要知道每个参数类型的实现。

让我们从第一个参数开始,它是一个1个string类型的slice, slice是Go中的引用类型。 这意味着slice的值是一个标题值,其中包含指向某些基础数据的指针。 在slice的情况下,标头值是三字结构,其包含指向底层阵列的指针,slice的长度和容量。 与切片标头关联的值由堆栈跟踪中的前三个值表示:

清单5

1
2
3
4
5
6
7
8
9
10
11
12
13
// Slice parameter value
slice := make([]string, 2, 4)

// Slice header values
Pointer: 0x2080c3f50
Length: 0x2
Capacity: 0x4

// Declaration
main.Example(slice []string, str string, i int)

// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

清单5 显示了堆栈跟踪中的前三个值如何与slice参数匹配。 第一个值表示指向底层字符串数组的指针。 用于初始化slice的长度和容量的数字与第二个和第三个值匹配 。这三个值表示切片标头的每个值,即Example函数的第一个参数。

Figure 1

现在让我们看一下第二个参数,它是一个string。 string也是引用类型,但此标头值是不可变的。 字符串的标头值被声明为两部分,包含指向底层字节数组的指针和字符串的长度:

清单6

1
2
3
4
5
6
7
8
9
10
11
12
// String parameter value
"hello"

// String header values
Pointer: 0x425c0
Length: 0x5

// Declaration
main.Example(slice []string, str string, i int)

// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

清单6显示了堆栈跟踪中的第四个和第五个值如何与string参数匹配。 第四个值表示指向底层字节数组的指针,第五个值表示字符串的长度为5。字符串

“hello”

需要5个字节。 这两个值表示字符串标题的每个值,即Example函数的第二个参数。

Figure 2

第三个参数是一个整数,它是一个单值:

清单7

1
2
3
4
5
6
7
8
9
10
11
// Integer parameter value
10

// Integer value
Base 16: 0xa

// Declaration
main.Example(slice []string, str string, i int)

// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

清单7显示了堆栈跟踪中的最后一个值如何与int类型的参数匹配。 trace中的最后一个值是十六进制数0xa,它的值是10.与该参数传递的值相同。 该值代表Example函数中的第三个参数。

Figure 3

Methods(方法的情况)

如果我们将Example作为结构体的方法会怎么样呢?

清单8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
01 package main
02
03 import "fmt"
04
05 type trace struct{}
06
07 func main() {
08 slice := make([]string, 2, 4)
09
10 var t trace
11 t.Example(slice, "hello", 10)
12 }
13
14 func (t *trace) Example(slice []string, str string, i int) {
15 fmt.Printf("Receiver Address: %p\n", t)
16 panic("Want stack trace")
17 }

清单8通过在第05行声明一个名为trace的新类型,并更改程序,将Example申明为trace类型的方法。通过使用trace类型的指针接收器重新声明该函数来完成转换。 然后在第10行,将变量t申明为trace类型,并且在第11行进行方法调用。

由于该方法是使用指针声明的,因此Go将获取变量t的地址来支持接收者类型,即使方法调用是使用值来完成的。 这次运行程序时,堆栈跟踪有点不同:

清单9

1
2
3
4
5
6
7
8
9
10
11
Receiver Address: 0x1553a8
panic: Want stack trace

01 goroutine 1 [running]:
02 main.(*trace).Example(0x1553a8, 0x2081b7f50, 0x2, 0x4, 0xdc1d0, 0x5, 0xa)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:16 +0x116

03 main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:11 +0xae

清单9中你应该注意的第一件事是第02行的堆栈跟踪清楚的显示这是一个使用指针接收器调用的方法。现在函数的名称显示的样子是: 在package名字和方法名之间多出了”*trace”字样 。 需要注意的第二件事是参数列表的第1个参数标明了结构体(t)地址。 我们从堆栈跟踪中看到了这个实现细节。

Packing(打包)

如果有多个参数可以填充到一个single word, 那么堆栈跟踪中参数的值将打包在一起 :

清单10

1
2
3
4
5
6
7
8
9
01 package main
02
03 func main() {
04 Example(true, false, true, 25)
05 }
06
07 func Example(b1, b2, b3 bool, i uint8) {
08 panic("Want stack trace")
09 }

这个例子修改Example函数改为接收4个参数:3个bool型和1个八位无符号整型。bool值也是用8个bit表示,所以在32位和64位架构下,4个参数可以合并为一个single word。 当程序运行时,它会产生一个有趣的堆栈跟踪 :

清单11

1
2
3
4
5
6
7
01 goroutine 1 [running]:
02 main.Example(0x19010001)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:8 +0x64
03 main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:4 +0x32

对于对Example的调用,堆栈跟踪中没有四个值,而是有一个值。所有四个单独的8位值都拼凑成一个单词:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Parameter values
true, false, true, 25

// Word value
Bits Binary Hex Value
00-07 0000 0001 01 true
08-15 0000 0000 00 false
16-23 0000 0001 01 true
24-31 0001 1001 19 25

// Declaration
main.Example(b1, b2, b3 bool, i uint8)

// Stack trace
main.Example(0x19010001)

清单12显示了堆栈跟踪中的值是如何与传入的所有四个参数值匹配.true的值是一个8位值,用1表示,false的值是0.二进制25的值是11001,转换为十六进制是19。 现在,我们看到堆栈信息中包括十六进制值,需要知道这些值是如何传递的。

结论

Go运行时提供了大量信息来帮助我们调试程序。在这篇文章中,我们专注于堆栈跟踪。分析在整个调用堆栈中传递给每个函数的值的能力是很有用的。它不止一次帮助我很快识别我的错误。既然您已经知道如何读取堆栈跟踪,那么希望您可以在下次发生堆栈跟踪时利用这些知识。

Consul安装部署

什么是Consul

  • 是一个服务管理软件。
  • 支持多数据中心下,分布式高可用的,服务发现和配置共享。
  • consul支持健康检查,允许存储键值对。
  • 一致性协议采用 Raft 算法,用来保证服务的高可用.
  • 成员管理和消息广播 采用GOSSIP协议,支持ACL访问控制。

ACL技术

在路由器中被广泛采用,它是一种基于包过滤的流控制技术。控制列表通过把源地址、目的地址及端口号作为数据包检查的基本元素,并可以规定符合条件的数据包是否允许通过。

gossip就是p2p协议。

他主要要做的事情是,去中心化。
这个协议就是模拟人类中传播谣言的行为而来。首先要传播谣言就要有种子节点。种子节点每秒都会随机向其他节点发送自己所拥有的节点列表,以及需要传播的消息。任何新加入的节点,就在这种传播方式下很快地被全网所知道。

什么是强一致性协议?

按照某一顺序串行执行存储对象读写操作, 更新存储对象之后, 后续访问总是读到最新值。 假如进程A先更新了存储对象,存储系统保证后续A,B,C进程的读取操作都将返回最新值。强一致性模型有几种常见实现方法, 主从同步复制, 以及quorum复制等。

Consul is opinionated in its usage while Serf is a more flexible and general purpose tool. In CAP terms, Consul uses a CP architecture, favoring consistency over availability.

官方文档地址

说明consul是cp的,并不是网上有些文章说的是ca模式

下面表格对consul 、zookeeper、 etcd、 euerka做了对比

Feature Consul zookeeper etcd euerka
服务健康检查 服务状态,内存,硬盘等 (弱)长连接,keepalive 连接心跳 可配支持
多数据中心 支持
kv存储服务 支持 支持 支持
一致性 raft paxos raft
cap cp cp cp ap
使用接口(多语言能力) 支持http和dns 客户端 http/grpc http(sidecar)
watch支持 全量/支持long polling 支持 支持 long polling 支持 long polling/大部分增量
自身监控 metrics metrics metrics
安全 acl /https acl https支持(弱)
spring cloud集成 已支持 已支持 已支持 已支持

Consul安装

安装Consul,找到适合你系统的包下载他.Consul打包为一个’Zip’文件.前往下载

下载后解开压缩包.拷贝Consul到你的PATH路径中,在Unix系统中/bin/usr/local/bin是通常的安装目录.根据你是想为单个用户安装还是给整个系统安装来选择.在Windows系统中有可以安装到%PATH%的路径中.

验证安装

完成安装后,通过打开一个新终端窗口检查consul安装是否成功.通过执行 consul你应该看到类似下面的输出

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
[root@iZbp14ouog5ocoeakj39q1Z ~]# consul
Usage: consul [--version] [--help] <command> [<args>]

Available commands are:
agent Runs a Consul agent
catalog Interact with the catalog
connect Interact with Consul Connect
event Fire a new event
exec Executes a command on Consul nodes
force-leave Forces a member of the cluster to enter the "left" state
info Provides debugging information for operators.
intention Interact with Connect service intentions
join Tell Consul agent to join cluster
keygen Generates a new encryption key
keyring Manages gossip layer encryption keys
kv Interact with the key-value store
leave Gracefully leaves the Consul cluster and shuts down
lock Execute a command holding a lock
maint Controls node or service maintenance mode
members Lists the members of a Consul cluster
monitor Stream logs from a Consul agent
operator Provides cluster-level tools for Consul operators
reload Triggers the agent to reload configuration files
rtt Estimates network round trip time between nodes
snapshot Saves, restores and inspects snapshots of Consul server state
validate Validate config files/directories
version Prints the Consul version
watch Watch for changes in Consul

如果你得到一个consul not be found的错误,你的PATH可能没有正确设置.请返回检查你的consul的安装路径是否包含在PATH中.

consule参数的介绍

consul 术语

首先介绍下在 consul 中会经常见到的术语:

  • node:节点,需要 consul 注册发现或配置管理的服务器。在一个集群中必须是唯一的,默认是该节点的主机名
  • agent:consul 中的核心程序,它将以守护进程的方式在各个节点运行,有 client 和 server 启动模式。每个 agent 维护一套服务和注册发现以及健康信息。
  • client:agent 以 client 模式启动的节点。在该模式下,该节点会采集相关信息,通过 RPC 的方式向 server 发送。这个地址提供HTTP、DNS、RPC等服务,默认是127.0.0.1所以不对外提供服务,如果你要对外提供服务改成0.0.0.0
  • server:agent 以 server 模式启动的节点。一个数据中心中至少包含 1 个 server 节点。不过官方建议使用 3 或 5 个 server 节点组建成集群,以保证高可用且不失效率。server 节点参与 Raft、维护会员信息、注册服务、健康检查等功能。
  • datacenter:数据中心,私有的,低延迟的和高带宽的网络环境。一般的多个数据中心之间的数据是不会被复制的,但可用过 ACL replication 或使用外部工具 onsul-replicate
  • Consensus共识协议,使用它来协商选出 leader。
  • Gossip:consul 是建立在 Serf,它提供完整的 gossip protocol维基百科
  • LAN Gossip,Lan gossip 池,包含位于同一局域网或数据中心上的节点。
  • WAN Gossip,只包含 server 的 WAN Gossip 池,这些服务器主要位于不同的数据中心,通常通过互联网或广域网进行通信。
  • members:成员,对 consul 成员的称呼。提供会员资格,故障检测和事件广播。
  • -bootstrap-expect :在一个datacenter中期望提供的server节点数目,当该值提供的时候,consul一直等到达到指定sever数目的时候才会引导整个集群,该标记不能和bootstrap共用
  • -bind:该地址用来在集群内部的通讯,集群内的所有节点到地址都必须是可达的,默认是0.0.0.0
  • -ui-dir: 提供存放web ui资源的路径,该目录必须是可读的,1.2中是直接使用-ui参数就可以
  • -rejoin:使consul忽略先前的离开,在再次启动后仍旧尝试加入集群中。
  • -config-dir::配置文件目录,里面所有以.json结尾的文件都会被加载

consul 端口说明

consul 内使用了很多端口,理解这些端口的用处对你理解 consul 架构很有帮助:

端口 说明
TCP/8300 8300 端口用于服务器节点。客户端通过该端口 RPC 协议调用服务端节点。服务器节点之间相互调用
TCP/UDP/8301 8301 端口用于单个数据中心所有节点之间的互相通信,即对 LAN 池信息的同步。它使得整个数据中心能够自动发现服务器地址,分布式检测节点故障,事件广播(如领导选举事件)。
TCP/UDP/8302 8302 端口用于单个或多个数据中心之间的服务器节点的信息同步,即对 WAN 池信息的同步。它针对互联网的高延迟进行了优化,能够实现跨数据中心请求。
8500 8500 端口基于 HTTP 协议,用于 API 接口或 WEB UI 访问。
8600 8600 端口作为 DNS 服务器,它使得我们可以通过节点名查询节点信息。

Consul运行

开发模式运行consul

  • 查看集群成员

新开一个终端窗口运行consul members, 你可以看到Consul集群的成员.

  • 浏览器查看webUI界面

    浏览器中输出serverip:8500,会出现consul的管理webUI

生产环境运行consul

启动三台server服务器

agent可以运行为server或client模式.每个数据中心至少必须拥有一台server . 建议在一个集群中有3或者5个server.部署单一的server,在出现失败时会不可避免的造成数据丢失.

1
其他的agent运行为client模式.一个client是一个非常轻量级的进程.用于注册服务,运行健康检查和转发对server的查询.agent必须在集群中的每个主机上运行.

这里启动三个agent server,三台机器的地址分别是

1
2
3
10.174.96.52    s1 
10.173.224.146 s2
10.173.224.74 s3

必须有一个初始节点,且手动指定为leader,然后开启其它server节点,让它们加入集群。 最后初始节点下线,重新加入集群,参与选举。

我们手动指定10.174.96.52为leader,这种方式,-bootstrap-expect 3 期待三个 server 加入才能完成 consul 的引导。

1
consul agent -server -bootstrap-expect 3 -data-dir /tmp/consul -node=s1 -bind=10.174.96.52 -ui -client 0.0.0.0

继续添加 server2、server3:

1
2
3
consul agent -server  -data-dir /tmp/consul -node=s2 -bind=10.173.224.146 -ui -join 10.174.96.52

consul agent -server -data-dir /tmp/consul -node=s3 -bind=10.173.224.74 -ui -join 10.174.96.52
  • -data-dir:提供一个目录用来存放agent的状态,所有的agent允许都需要该目录,该目录必须是稳定的,系统重启后都继续存在
  • -join:将agent加入到集群

此时10.174.96.52显示的信息是,当146主机和74主机加入到集群后,s1就被选举为leader

10.173.224.146加入到集群中后显示的信息

查看webUI

查看三个server的名称

将新服务服务注册到consul

consul 支持两种服务发现的方式:

  1. 通过 HTTP API 方式,这种方式需要额外编程,适用于不安装 consul agent 的情况,文档地址
  2. 通过 consul agent 配置的方式,agent 启动的时候会读取一个配置文件目录,通过配置进行服务的发现,文档地址

这里介绍第二种方式,通过配置文件来进行服务发现。这里就需要用到我们的 client 服务器啦。

首先,用 Go 写一个简单的 HTTP 服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"net/http"
)

func HandleExample(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello man"))
}

func HandleHealth(w http.ResponseWriter, r *http.Request) {
fmt.Println("health check!")
}

func main() {
http.HandleFunc("/", HandleExample)
http.HandleFunc("/health", HandleHealth)

fmt.Println("listen on :9000")
http.ListenAndServe(":9000", nil)
}

然后编辑一个配置文件 /etc/consul.d/web.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"service":
{
"name": "web",
"tags": ["primary"],
"address": "10.174.96.52",
"port": 9000,
"checks": [
{
"http": "http://localhost:9000/health",
"interval": "10s"
}]
}
}

  • -config-dir:配置文件目录,里面所有以.json结尾的文件都会被加载

配置共享

由与有了 agent client 和 server 模式的提供,配置共享也变得异常的简单。

在任意节点更新配置数据:

1
2
$ consul kv put redis/config 192.168.99.133
Success! Data written to: redis/config

整个集群均会自动更新,在 s1 节点查看数据:

1
2
$ consul kv get redis/config
192.168.99.133

断开连接

你可以使用Ctrl-C 优雅的关闭Agent. 中断Agent之后你可以看到他离开了集群并关闭.

在退出中,Consul提醒其他集群成员,这个节点离开了.如果你强行杀掉进程.集群的其他成员应该能检测到这个节点失效了.当一个成员离开,他的服务和检测也会从目录中移除.当一个成员失效了,他的健康状况被简单的标记为危险,但是不会从目录中移除.Consul会自动尝试对失效的节点进行重连.允许他从某些网络条件下恢复过来.离开的节点则不会再继续联系.

此外,如果一个agent作为一个服务器,一个优雅的离开是很重要的,可以避免引起潜在的可用性故障影响达成一致性协议.

查看这里了解添加和移除server.

总结

一个consul agent就是一个独立的程序。一个长时间运行的守护进程,运行在concul集群中的每个节点上。

启动一个consul agent ,只是启动一个孤立的node,如果想知道集群中的其他节点,应该将consul agent加入到集群中去 cluster。

agent有两种模式:server与client。

  • server模式包含了一致性的工作:保证一致性和可用性(在部分失败的情况下),响应RPC,同步数据到其他节点代理。
  • client 模式用于与server进行通信,转发RPC到服务的代理agent,它仅保存自身的少量一些状态,是非常轻量化的东西。本身是相对无状态的。

agent除去设置server/client模式、数据路径之外,还最好设置node的名称和ip。

一张经典的consul架构图片:

  • LAN gossip pool包含了同一局域网内所有节点,包括server与client。这基本上是位于同一个数据中心DC。
  • WAN gossip pool一般仅包含server,将跨越多个DC数据中心,通过互联网或广域网进行通信。
  • Leader服务器负责所有的RPC请求,查询并相应。所以其他服务器收到client的RPC请求时,会转发到leader服务器。

参考:

consul 支持多数据中心的服务发现与配置共享工具

consul入门

Consul使用手册

|