MySQL DBA利器innodb_ruby

innodb_ruby简介

innodb_ruby是一款用ruby写的用来分析 innodb 物理文件的专业DBA工具,可以通过这款工具来窥探innodb内部的一些结构。
注意不要在生产环境中使用此工具,以避对线上服务造成影响。官方网址 https://rubygems.org/gems/innodb_ruby

注意如果(Linux)平台安装中遇到错误一般情况是由于缺少依赖库造成的,可以先安装 sudo apt-get install libxslt1-dev libxml2-dev 相关库。

命令语法

在执行以下命令时,建议切换到MySQL 的 datadir 目录里。

sxf@ubuntu:~$ innodb_space --help

Usage: innodb_space <options> <mode>
innodb_space <选项> <模式>
命令主要分 options 和 mode 两大部分。

Invocation examples:

  innodb_space -s ibdata1 [-T tname [-I iname]] [options] <mode>
    Use ibdata1 as the system tablespace and load the tname table (and the
    iname index for modes that require it) from data located in the system
    tablespace data dictionary. This will automatically generate a record
    describer for any indexes.

    参数:
    -s 参数指的是系统表空间文件 ibdata1, 这个一般在datadir目录里可以找到。
    -T 数据表名称,一般为数据库其中一个表的物理文件路径
    -I 表示索引的名称, 如果是主键的话,直接填写 -I PRIMARY 即可,此时可省略此参数

    如 innodb_space -s ibdata1 -T lab/tb space-indexes,则表示查看lab数据库的tb表的索引统计信息


  innodb_space -f tname.ibd [-r ./desc.rb -d DescClass] [options] <mode>
    Use the tname.ibd table (and the DescClass describer where required).

The following options are supported:

  --help, -?
    Print this usage text.

  --trace, -t
    Enable tracing of all data read. Specify twice to enable even more
    tracing (including reads during opening of the tablespace) which can
    be quite noisy.

  --system-space-file, -s <arg>
    Load the system tablespace file or files <arg>: Either a single file e.g.
    "ibdata1", a comma-delimited list of files e.g. "ibdata1,ibdata1", or a
    directory name. If a directory name is provided, it will be scanned for all
    files named "ibdata?" which will then be sorted alphabetically and used to
    load the system tablespace.

  --table-name, -T <name>
    Use the table name <name>.
    表名

  --index-name, -I <name>
    Use the index name <name>.
    索引名

  --space-file, -f <file>
    Load the tablespace file <file>.

  --page, -p <page>
    Operate on the page <page>.
    页数

  --level, -l <level>
    Operate on the level <level>.
    索引树层级数,一般不会超过3

  --list, -L <list>
    Operate on the list <list>.

  --fseg-id, -F <fseg_id>
      Operate on the file segment (fseg) <fseg_id>.

  --require, -r <file>
    Use Ruby's "require" to load the file <file>. This is useful for loading
    classes with record describers.

  --describer, -d <describer>
    Use the named record describer to parse records in index pages.

The following modes are supported:
模式项列表

  系统表空间
  system-spaces
    Print a summary of all spaces in the system.
    

  数据字典表(information_schema中数据库SYS_TABLES表内容,下同)
  data-dictionary-tables
    Print all records in the SYS_TABLES data dictionary table.

  data-dictionary-columns
    Print all records in the SYS_COLUMNS data dictionary table.

  data-dictionary-indexes
    Print all records in the SYS_INDEXES data dictionary table.

  data-dictionary-fields
    Print all records in the SYS_FIELDS data dictionary table.

  
  汇总表空间中的所有页信息,需要使用 --page/-p 参数指定页数
  space-summary
    Summarize all pages within a tablespace. A starting page number can be
    provided with the --page/-p argument.

  汇总表空间中的所有索引页信息,对于分析每个页记录填充率情况的时候很有用,同样需要使用--page/-p指定页数
  space-index-pages-summary
    Summarize all "INDEX" pages within a tablespace. This is useful to analyze
    page fill rates and record counts per page. In addition to "INDEX" pages,
    "ALLOCATED" pages are also printed and assumed to be completely empty.
    A starting page number can be provided with the --page/-p argument.

  与space-index-pages-summary差不多,但只显示一些摘要信息,需要配合参数一块使用
  space-index-fseg-pages-summary
    The same as space-index-pages-summary but only iterate one fseg, provided
    with the --fseg-id/-F argument.

  space-index-pages-free-plot
    Use Ruby's gnuplot module to produce a scatterplot of page free space for
    all "INDEX" and "ALLOCATED" pages in a tablespace. More aesthetically
    pleasing plots can be produced with space-index-pages-summary output,
    but this is a quick and easy way to produce a passable plot. A starting
    page number can be provided with the --page/-p argument.

  遍历空间中的所有页面,统计每个类型的页共占用了多少页
  space-page-type-regions
    Summarize all contiguous regions of the same page type. This is useful to
    provide an overall view of the space and allocations within it. A starting
    page number can be provided with the --page/-p argument.

  按类型汇总所有页面信息
  space-page-type-summary
    Summarize all pages by type. A starting page number can be provided with
    the --page/-p argument.

  表空间中所有索引统计信息(系统空间或每个文件表空间)
  space-indexes
    Summarize all indexes (actually each segment of the indexes) to show
    the number of pages used and allocated, and the segment fill factor.

  space-lists
    Print a summary of all lists in a space.

  space-list-iterate
    Iterate through the contents of a space list.

  space-extents
    Iterate through all extents, printing the extent descriptor bitmap.

  space-extents-illustrate
    Iterate through all extents, illustrating the extent usage using ANSI
    color and Unicode box drawing characters to show page usage throughout
    the space.

  space-extents-illustrate-svg
    Iterate through all extents, illustrating the extent usage in SVG format
    printed to stdout to show page usage throughout the space.

  space-lsn-age-illustrate
    Iterate through all pages, producing a heat map colored by the page LSN
    using ANSI color and Unicode box drawing characters, allowing the user to
    get an overview of page modification recency.

  space-lsn-age-illustrate-svg
    Iterate through all pages, producing a heat map colored by the page LSN
    producing SVG format output, allowing the user to get an overview of page
    modification recency.

  space-inodes-fseg-id
    Iterate through all inodes, printing only the FSEG ID.

  space-inodes-summary
    Iterate through all inodes, printing a short summary of each FSEG.

  space-inodes-detail
    Iterate through all inodes, printing a detailed report of each FSEG.

  通过递归整个B+树(通过递归扫描所有页面,而不仅仅是按列表的叶子页面)来执行索引扫描(执行完整索引扫描)
  index-recurse
    Recurse an index, starting at the root (which must be provided in the first
    --page/-p argument), printing the node pages, node pointers (links), leaf
    pages. A record describer must be provided with the --describer/-d argument
    to recurse indexes (in order to parse node pages).

  将索引作为索引递归进行递归处理,但在索引页中打印每条记录的偏移量
  index-record-offsets
    Recurse an index as index-recurse does, but print the offsets of each
    record within the page.

  index-digraph
    Recurse an index as index-recurse does, but print a dot-compatible digraph
    instead of a human-readable summary.

  打印指定 level 级别的所有page信息
  index-level-summary
    Print a summary of all pages at a given level (provided with the --level/-l
    argument) in an index.

  index-fseg-internal-lists
  index-fseg-leaf-lists
    Print a summary of all lists in an index file segment. Index root page must
    be provided with --page/-p.

  index-fseg-internal-list-iterate
  index-fseg-leaf-list-iterate
    Iterate the file segment list (whose name is provided in the first --list/-L
    argument) for internal or leaf pages for a given index (whose root page
    is provided in the first --page/-p argument). The lists used for each
    index are "full", "not_full", and "free".

  index-fseg-internal-frag-pages
  index-fseg-leaf-frag-pages
    Print a summary of all fragment pages in an index file segment. Index root
    page must be provided with --page/-p.

  page-dump
    Dump the contents of a page, using the Ruby pp ("pretty-print") module.

  page-account
    Account for a page's usage in FSEGs.

  page-validate
    Validate the contents of a page.

  页目录字典记录
  page-directory-summary
    Summarize the record contents of the page directory in a page. If a record
    describer is available, the key of each record will be printed.

  对一个页的所有记录进行汇总
  page-records
    Summarize all records within a page.

  详细说明一个页面的内容,并且根据类型进行着色显示
  page-illustrate
    Produce an illustration of the contents of a page.

  record-dump
    Dump a detailed description of a record and the data it contains. A record
    offset must be provided with -R/--record.

  record-history
    Summarize the history (undo logs) for a record. A record offset must be
    provided with -R/--record.

  undo-history-summary
    Summarize all records in the history list (undo logs).

  undo-record-dump
    Dump a detailed description of an undo record and the data it contains.
    A record offset must be provided with -R/--record.

参数详解

测试数据库 lab ,表名 tb ,表结构如下,

CREATE TABLE `tb` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `num` int(11) NOT NULL,
  `age` tinyint(1) unsigned DEFAULT '13',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=40001 DEFAULT CHARSET=latin1;

这里先添加了3万多的测试数据。

系统表空间 system-spaces

root@ubuntu:/var/lib/mysql# innodb_space -s ibdata1 -T lab/tb system-spaces
name                            pages       indexes
(system)                        768         7
lab/tb                          448         1
mysql/engine_cost               6           1
mysql/gtid_executed             6           1
mysql/help_category             7           2
mysql/help_keyword              15          2
mysql/help_relation             9           1
mysql/help_topic                576         2
mysql/innodb_index_stats        6           1
mysql/innodb_table_stats        6           1
mysql/plugin                    6           1
mysql/server_cost               6           1
mysql/servers                   6           1
mysql/slave_master_info         6           1
mysql/slave_relay_log_info      6           1
mysql/slave_worker_info         6           1
mysql/time_zone                 6           1
mysql/time_zone_leap_second     6           1
mysql/time_zone_name            6           1
mysql/time_zone_transition      6           1
mysql/time_zone_transition_type 6           1
sys/sys_config                  6           1

innodb_space列出所有物理对象的数量。这些文件一般在相应数据库中可以找到扩展名为.ibd 的文件,如 sys库的sys_config.ibd文件

索引结构、数据分配情 space-indexes

root@ubuntu:/var/lib/mysql# innodb_space -s ibdata1 -T lab/tb space-indexes
id          name                            root        fseg        fseg_id     used        allocated   fill_factor
43          PRIMARY                         3           internal    1           1           1           100.00%
43          PRIMARY                         3           leaf        2           75          96          78.12%

列说明:
name:索引的名称,PRIMARY代表的就是聚集索引,因为InnoDB表是聚集所以组织表,行记录就是聚集索引;idx_c就是辅助索引的名称。
root:索引中根节点的page号;可以看出聚集索引的根节点是第3个page(为什么是从第三个page开始,看下文space-page-type-regions),辅助索引的根节点是第4个page。
fseg:page的说明,internal表示非叶子节点或属于根节点,leaf表示叶子节点(也就是数据页)。
used:索引使用了多少个page,可以看出聚集索引的根节点点使用了1个page,叶子节点使用了3个page;辅助索引idx_c的叶子节点使用了1个page。
allocated:索引分配了多少个page,可以看出聚集索引的根节点分配了1个page,叶子节点分配了3个page;辅助索引idx_c的叶子节点分配了1个page
fill_factor:索引的填充度,所有的填充度都是100%。

遍历空间中的所有页面,统计每个类型的页共占用了多少 space-page-type-regions

start       end         count       type
0           0           1           FSP_HDR
1           1           1           IBUF_BITMAP
2           2           1           INODE
3           5           3           INDEX
6           6           1           FREE (INDEX)
7           36          30          INDEX
37          63          27          FREE (ALLOCATED)
64          106         43          INDEX
107         127         21          FREE (ALLOCATED)

列说明:
start:从第几个page开始。
end:从第几个page结束。
count:占用了多少个page。
type:page的类型。

从上面的结果可以看出:“FSP_HDR”、“IBUF_BITMAP”、“INODE”是分别占用了0,1,2号的page,从3号page开始才是存放数据和索引的页(Index)
接下来,根据得到的聚集索引和辅助索引的根节点来获取索引上的其他page的信息。

索引级数统计信息 index-level-summary

root@ubuntu:/var/lib/mysql# innodb_space -s ibdata1 -T lab/tb -I PRIMARY -l 0 index-level-summary
page    index   level   data    free    records min_key
4       43      0       14952   1036    534     id=1
5       43      0       14952   1036    534     id=535
7       43      0       14952   1036    534     id=1069
8       43      0       14952   1036    534     id=1603
9       43      0       14952   1036    534     id=2137
10      43      0       14952   1036    534     id=2671
11      43      0       14952   1036    534     id=3205
12      43      0       14952   1036    534     id=3739
13      43      0       14952   1036    534     id=4273
14      43      0       14952   1036    534     id=4807
15      43      0       14952   1036    534     id=5341
16      43      0       14952   1036    534     id=5875
17      43      0       14952   1036    534     id=6409
18      43      0       14952   1036    534     id=6943
19      43      0       14952   1036    534     id=7477
20      43      0       14952   1036    534     id=8011
21      43      0       14952   1036    534     id=8545
22      43      0       14952   1036    534     id=9079
23      43      0       14952   1036    534     id=9613
24      43      0       14952   1036    534     id=10147
25      43      0       14952   1036    534     id=10681
26      43      0       14952   1036    534     id=11215
27      43      0       14952   1036    534     id=11749
28      43      0       14952   1036    534     id=12283
29      43      0       14952   1036    534     id=12817
30      43      0       14952   1036    534     id=13351
31      43      0       14952   1036    534     id=13885
32      43      0       14952   1036    534     id=14419
33      43      0       14952   1036    534     id=14953
34      43      0       14952   1036    534     id=15487
35      43      0       14952   1036    534     id=16021
36      43      0       14952   1036    534     id=16555
64      43      0       14952   1036    534     id=17089
65      43      0       14952   1036    534     id=17623
66      43      0       14952   1036    534     id=18157
67      43      0       14952   1036    534     id=18691
68      43      0       14952   1036    534     id=19225
69      43      0       14952   1036    534     id=19759
70      43      0       14952   1036    534     id=20293
71      43      0       14952   1036    534     id=20827
72      43      0       14952   1036    534     id=21361
73      43      0       14952   1036    534     id=21895
74      43      0       14952   1036    534     id=22429
75      43      0       14952   1036    534     id=22963
76      43      0       14952   1036    534     id=23497
77      43      0       14952   1036    534     id=24031
78      43      0       14952   1036    534     id=24565
79      43      0       14952   1036    534     id=25099
80      43      0       14952   1036    534     id=25633
81      43      0       14952   1036    534     id=26167
82      43      0       14952   1036    534     id=26701
83      43      0       14952   1036    534     id=27235
84      43      0       14952   1036    534     id=27769
85      43      0       14952   1036    534     id=28303
86      43      0       14952   1036    534     id=28837
87      43      0       14952   1036    534     id=29371
88      43      0       14952   1036    534     id=29905
89      43      0       14952   1036    534     id=30439
90      43      0       14952   1036    534     id=30973
91      43      0       14952   1036    534     id=31507
92      43      0       14952   1036    534     id=32041
93      43      0       14952   1036    534     id=32575
94      43      0       14952   1036    534     id=33109
95      43      0       14952   1036    534     id=33643
96      43      0       14952   1036    534     id=34177
97      43      0       14952   1036    534     id=34711
98      43      0       14952   1036    534     id=35245
99      43      0       14952   1036    534     id=35779
100     43      0       14952   1036    534     id=36313
101     43      0       14952   1036    534     id=36847
102     43      0       14952   1036    534     id=37381
103     43      0       14952   1036    534     id=37915
104     43      0       14952   1036    534     id=38449
105     43      0       14952   1036    534     id=38983
106     43      0       13552   2460    484     id=39517
root@ubuntu:/var/lib/mysql# innodb_space -s ibdata1 -T lab/tb -I PRIMARY -l 1 index-level-summary
page    index   level   data    free    records min_key
3       43      1       1050    15168   75      id=1
root@ubuntu:/var/lib/mysql# innodb_space -s ibdata1 -T lab/tb -I PRIMARY -l 2 index-level-summary
page    index   level   data    free    records min_key

这里我们分别查看了0、1和2级别的信息,但2级别是没有任何信息输出的,所以这里的索引树高度是2。

列说明:

page 页数,可以看到并不一定是连续的
index 待确认
level 级数
data 数据大小
free 空闲大小
records 记录个数
min_key 最小记录id,每个page都会有一个最小记录id,二分法查找记录时使用.

查看汇总页记录 page-records

root@ubuntu:/var/lib/mysql# innodb_space -s ibdata1 -T lab/tb -p 3 page-records
Record 126: (id=1) → #4
Record 140: (id=535) → #5
Record 154: (id=1069) → #7
Record 168: (id=1603) → #8
Record 182: (id=2137) → #9
Record 196: (id=2671) → #10
Record 210: (id=3205) → #11
Record 224: (id=3739) → #12
Record 238: (id=4273) → #13
Record 252: (id=4807) → #14
Record 266: (id=5341) → #15
Record 280: (id=5875) → #16
Record 294: (id=6409) → #17
Record 308: (id=6943) → #18
Record 322: (id=7477) → #19
Record 336: (id=8011) → #20
Record 350: (id=8545) → #21
Record 364: (id=9079) → #22
Record 378: (id=9613) → #23
Record 392: (id=10147) → #24
Record 406: (id=10681) → #25
Record 420: (id=11215) → #26
Record 434: (id=11749) → #27
Record 448: (id=12283) → #28
Record 462: (id=12817) → #29
Record 476: (id=13351) → #30
Record 490: (id=13885) → #31
Record 504: (id=14419) → #32
Record 518: (id=14953) → #33
Record 532: (id=15487) → #34
Record 546: (id=16021) → #35
Record 560: (id=16555) → #36
Record 574: (id=17089) → #64
Record 588: (id=17623) → #65
Record 602: (id=18157) → #66
Record 616: (id=18691) → #67
Record 630: (id=19225) → #68
Record 644: (id=19759) → #69
Record 658: (id=20293) → #70
Record 672: (id=20827) → #71
Record 686: (id=21361) → #72
Record 700: (id=21895) → #73
Record 714: (id=22429) → #74
Record 728: (id=22963) → #75
Record 742: (id=23497) → #76
Record 756: (id=24031) → #77
Record 770: (id=24565) → #78
Record 784: (id=25099) → #79
Record 798: (id=25633) → #80
Record 812: (id=26167) → #81
Record 826: (id=26701) → #82
Record 840: (id=27235) → #83
Record 854: (id=27769) → #84
Record 868: (id=28303) → #85
Record 882: (id=28837) → #86
Record 896: (id=29371) → #87
Record 910: (id=29905) → #88
Record 924: (id=30439) → #89
Record 938: (id=30973) → #90
Record 952: (id=31507) → #91
Record 966: (id=32041) → #92
Record 980: (id=32575) → #93
Record 994: (id=33109) → #94
Record 1008: (id=33643) → #95
Record 1022: (id=34177) → #96
Record 1036: (id=34711) → #97
Record 1050: (id=35245) → #98
Record 1064: (id=35779) → #99
Record 1078: (id=36313) → #100
Record 1092: (id=36847) → #101
Record 1106: (id=37381) → #102
Record 1120: (id=37915) → #103
Record 1134: (id=38449) → #104
Record 1148: (id=38983) → #105
Record 1162: (id=39517) → #106

每一行代表一个page记录,id=1表示这个表中的记录最小主键id=1, #4则表示在页号是4。

上面我们使用 index-level-summary 查看的level 1级别的索引page 3中共有75条记录,最小id为1,这里通过 page-records确认了这一点。

这里查看的是聚集索引(主键索引),如果是普通索引的话,会看到打印内容有一些不一样,类似于 RECORD: (age=21) → (id=100) 这种的,即指向了主键值。

现在我们在看一下page 4中的内容

root@ubuntu:/var/lib/mysql# innodb_space -s ibdata1 -T lab/tb -p 4 page-records | head
Record 126: (id=1) → (num=1, age=13)
Record 154: (id=2) → (num=2, age=13)
Record 182: (id=3) → (num=3, age=13)
Record 210: (id=4) → (num=4, age=13)
Record 238: (id=5) → (num=5, age=13)

我们发现输出的内容与page 3 的有些不一样,这里输出的是完整的详情记录,但page 3是一个一条记录与页的对应关系,我们一般称其为页目录。

推荐阅读

http://vlambda.com/wz_xeipHG6Q3r.html
https://www.cnblogs.com/cnzeno/p/6322842.html
https://blog.csdn.net/weixin_34368949/article/details/91381989
https://www.jianshu.com/p/c51873ea129a

利用jenkins+github实现应用的自动部署及回滚

对于jenkins的介绍这里不再详细写了,此教程只是为了让大家对部署和回滚原理有所了解。

一、创建项目

点击左侧的“New Item”,输入项目名称,如 rollback-demo。

选中 ” 丢弃旧的构建(Discard old builds)”项,在“策略(Strategy” 选择”Log Rotation“, 并输入保留的最大构建个数。

二、常规配置

设置参数,点击”Add Parameter“,依次选择 “Choice Parameter” 和 “String Parameter“这两,填写如下

这里的Name 项为参数名称,用户在操作的时候,会在deploy 和 rollback 两个值中选择一项。

三、源码管理

我们这里选择Git.并填写github.com上的项目地址,记得设置认证 Credentials。构建分支直接使用默认的 */master 即可以了。查看代码浏览器选择 githubweb,并填写项目的github地址。

四、构建触发事件

选择 “GitHub hook trigger for GITScm polling”,表示使用github webhook来触发构建操作,要实现引功能,需要在项目地址github.com里的“setting”里添加一个webhook的url地址,一般地址为http://jenkins.com/github-webhook/

同时为了防止网络通讯不稳定的情况,同时选择 “Poll SCM”, 在调度Schedule 杠中填写 H/5 * * * *,表示5分钟自动从github上拉取数据一次,如果有变化就进行构建。

五、构建环境

我们演示为了简单,使用了php项目,这里不进行任何操作。如果java、NodeJS或者Golang的话,可能需要进行一些操作, 有时为了方便会把这些操作放在下一步shell脚本里进行。

六、构建配置

1.添加构建步骤,点击Add build step,选择“Execute shell”,填写内容如下

#!/bin/bash
case $deploy_env in
deploy)
	echo "deploy $deploy_env"
    ;;
rollback)
	echo "rollback $deploy_env version=$version"
    cp -R ${JENKINS_HOME}/jobs/rollback-demo/builds/${version}/archive/*.* ./
    pwd &amp;&amp; ls
    ;;
    *)
    exit
    ;;
esac    

可以看到当rollback的时候,是从原来构建归档路径里把文件复制出来。

这里正常情况下应该有一些单元测试之类的脚本,这里省略不写了。

2.添加构建后的操作。点击“Add post-build action” -> “Archive the artifacts” 配置用于归档的文件为“**/*“,表示所有文件。这点十分重要,只有每次构建完归档了才有东西回滚,另外时间长了,归档的内容越来越多,所以上面设置了最大归档个数。
3.再次添加”Send build artifacts over SSH”,配置内容如下

注意 shell脚本里的路径比 Remote directory 的路径里多一个/data 目录,这是由于在配置 ssh server 的时候指定了一个根目录为 /data.

如果在 Add post-build action 中找不到send build artifacts over ssh ,则说明需要安装一下插件,左侧点击“Manage Jenkins”-> “Manage Plugis”, 搜索“Publish Over SSH”安装即可。

到这里为了基本配置完成了。

测试配置

这里我们首次手动构建一次,点击项目页面左侧菜单的“build with Parameter”,显示如下

在正常deploy的时候,version字段时忽略掉即可。如果要加滚的话,则需要选择”rollback”,同时填写 version字段号,这个字段号为页面左下角build history的编号,就是以#开始的那些数字

golang中几种对goroutine的控制方法

我们先看一个代码片断

func listen() {
	ticker := time.NewTicker(time.Second)
	for {
		select {
		case <-ticker.C:
			fmt.Println(time.Now())
		}
	}
}
func main() {
	go listen()
	time.Sleep(time.Second * 5)
	fmt.Println("main exit")
}

非常简单的一个goroutine用法,想必每个go新手都看过的。

不过在实际生产中,我们几乎看不到这种用法的的身影,原因很简单,我们无法实现对goroutine的控制,而一般业务中我们需要根据不同情况对goroutine进行各种操作。

要实现对goroutine的控制,一般有以下两种。

一、手动发送goroutine控制信号

这里我们发送一个退出goroutine的信号。

// listen 利用只读chan控制goroutine的退出
func listen(ch <-chan bool) {
	ticker := time.NewTicker(time.Second)
	for {
		select {
		case <-ticker.C:
			fmt.Println(time.Now())
		case <-ch:
			fmt.Println("goroutine exit")
			return
		}
	}
}

func main() {
	// 声明一个控制goroutine退出的chan
	ch := make(chan bool, 1)
	go listen(ch)

	// 只写chan
	func(ch chan<- bool) {
		time.Sleep(time.Second * 3)
		fmt.Println("发送退出chan信号")
		ch <- true
		close(ch)
	}(ch)

	time.Sleep(time.Second * 5)
	fmt.Println("main exit")
}

我们在main函数里发送一个控制goroutine的退出信息,在goroutine里我们会select这个通道,如果收到此信息,则直接退出goroutine。这里我们使用到了单向chan。

二、利用 context 包来控制goroutine

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
	defer cancel()

	go func() {
		ticker := time.NewTicker(time.Second)
		for {
			select {
			case <-ticker.C:
				fmt.Println(time.Now())
			case <-ctx.Done():
				// 3秒后会收到信号
				fmt.Println("goroutine exit")
				return
			}
		}
	}()

	time.Sleep(time.Second * 5)
	fmt.Println("main exit")
}

这里主要用到了上下文包context来实现,如果你看过包源码的话会发现 ctx.Done() 其实也是chan的读取操作,其原理和第一种方法是完全一样。

Golang遍历切片删除元素引起恐慌问题

删除一个切片的一些元素,https://github.com/golang/go/wiki/SliceTricks告知切片操作:Golang遍历切片恐慌时删除元素

a = append(a[:i], a[i+1:]...)

然后我下面的编码:

package main 

import (
    "fmt" 
) 

func main() { 
    slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 
    for i, value := range slice { 
     if value%3 == 0 { // remove 3, 6, 9 
      slice = append(slice[:i], slice[i+1:]...) 
     } 
    } 
    fmt.Printf("%vn", slice) 
} 

go run hello.go,它恐慌:

panic: runtime error: slice bounds out of range 

goroutine 1 [running]: 
panic(0x4ef680, 0xc082002040) 
    D:/Go/src/runtime/panic.go:464 +0x3f4 
main.main() 
    E:/Code/go/test/slice.go:11 +0x395 
exit status 2 

我该如何更改此代码才能正确使用?

想到的有几下几种方法

1、使用goto和标签

func main() { 
    slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 
Label: 
    for i, n := range slice { 
     if n%3 == 0 { 
      slice = append(slice[:i], slice[i+1:]...) 
      goto Label 
     } 
    } 
    fmt.Printf("%vn", slice) 
} 

它的工作原理很简单,就是不停的迭代,每次删除一个元素后,都需要从切片的开头重新迭代,太过于效率低下了,特别是当一个切片很大的情况下。

2,使用另一个变量临时存储想要的元素

func main() { 
    slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 
    dest := slice[:0] 
    for _, n := range slice { 
     if n%3 != 0 { // filter 
      dest = append(dest, n) 
     } 
    } 
    slice = dest 
    fmt.Printf("%vn", slice) 
} 

这种方法有些浪费内存,一是申请变量占用内存,另外切片扩大时,底层会重新申请内存空间,并将数据迁移过去。

3,从Remove elements in slice,与len操作:

func main() { 
    slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 
    for i := 0; i < len(slice); i++ { 
     if slice[i]%3 == 0 { 
      slice = append(slice[:i], slice[i+1:]...) 
      i-- // should I decrease index here? 
     } 
    } 
    fmt.Printf("%vn", slice) 
} 

要选哪一个,这里看一下基准测试

与基准:

func BenchmarkRemoveSliceElementsBySlice(b *testing.B) { 
    for i := 0; i < b.N; i++ { 
     slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 
     dest := slice[:0] 
     for _, n := range slice { 
      if n%3 != 0 { 
       dest = append(dest, n) 
      } 
     } 
    } 
} 

func BenchmarkRemoveSliceElementByLen(b *testing.B) { 
    for i := 0; i < b.N; i++ { 
     slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 
     for i := 0; i < len(slice); i++ { 
      if slice[i]%3 == 0 { 
       slice = append(slice[:i], slice[i+1:]...) 
      } 
     } 
    } 
} 


$ go test -v -bench=".*" 
testing: warning: no tests to run 
PASS 
BenchmarkRemoveSliceElementsBySlice-4 50000000    26.6 ns/op 
BenchmarkRemoveSliceElementByLen-4  50000000    32.0 ns/op 

从结果上看来使用第2种方法好像好一些的。还哪更好的解决办法没有了呢,网友给出了一种更合理的解决方案,也是使用迭代。

最佳方案:

package main 

import "fmt" 

func main() { 
    slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 

    k := 0 
    for _, n := range slice { 
     if n%3 != 0 { // filter 
      slice[k] = n 
      k++ 
     } 
    } 
    slice = slice[:k] 

    fmt.Println(slice) //[1 2 4 5 7 8] 
} 

实现原理就是将要保留的数据向左边存储,参考:https://play.golang.org/p/eMZltc_gEB,不得不说这个方法真是让人脑洞大开。

来源: https://stackoverflow.com/questions/38387633/golang-remove-elements-when-iterating-over-slice-panics/38387701#38387701

package main 

import "fmt" 

func main() { 
    slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 

    k := 0 
    for i, n := range slice { 
     if n%3 != 0 { // filter 
      if i != k { 
       slice[k] = n 
      } 
      k++ 
     } 
    } 
    slice = slice[:k] 

    fmt.Println(slice) //[1 2 4 5 7 8] 
} 

如果您需要新片保留旧切片

package main 

import "fmt" 

func main() { 
    slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 

    s2 := make([]int, len(slice)) 
    k := 0 
    for _, n := range slice { 
     if n%3 != 0 { // filter 
      s2[k] = n 
      k++ 
     } 
    } 
    s2 = s2[:k] 

    fmt.Println(s2) //[1 2 4 5 7 8] 
} 

http://cn.voidcc.com/question/p-mkbvfagj-hy.html

Golang中select用法导致CPU占用100%的问题分析

上一节(golang中有关select的几个知识点)中介绍了一些对于select{}的一些用法,今天介绍一下有关select在for语句中由于使用不当引起的CPU占用100% 的案例。

先看代码

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 10)

	// 读取chan
	go func() {
		for {
			select {
			case i := <-ch:
				// 只读取15次chan
				fmt.Println(i)
			default:
			}
		}
	}()

	// 写入10个值到chan
	for i := 0; i < 15; i++ {
		ch <- i
	}

	// 模拟程序效果使用
	time.Sleep(time.Minute)
}

实现功能

通过操作chan来实现消费者逻辑。

问题现象

但在运行的时候,即发现CPU占用率100%,下面我们分析一下什么原因引起的。

问题分析

程序运行时,先使用go关键字创建一个 goroutine,里面是一个for循环语句。for语句里面通过select{}来监听是否有chan的IO操作,当ch中有可以读取的数据时,则将值打印出来。没有的话则执行default语句,而这里default语句为空,所以继续下一次for语句,for{}是一个死循环语句。

当读取15次ch后,由于ch会永远处于阻塞状态,所以会一直执行default条件,然后再执行for循环。此时这段逻辑基本演变成了一个空的 for{} 语句,所以会导致CPU战胜100%。

解决办法

既然我们知道这个goroutine会一直占用cpu不放,我们只需要让当前goroutine出让CPU控制权给其它goroutine即可。根据GMP调度原理,这里我们只需要让操作chan的IO语句进行阻塞即可,这样P就可以继续寻找下一个goroutine执行了,等这个G和M有数据交互发生的时候,再找一个P继续执行就行了。这里实现方法很简单就注释掉 default 语句即可。

基于 GitHub Actions 实现 Golang 项目的自动构建部署

前几天 GitHub官网宣布 GitHub 的所有核心功能对所有人都免费开放,不得不说自从微软收购了GitHub后,确实带来了一些很大的改变。

以前有些项目考虑到协作关系的原因,虽然放在github上面,但对于一些项目的持续构建和部署一般是通过自行抢建Travis CI、jenkins等系统来实现。虽然去年推出了Actions用来代替它类三方系统,但感觉着还是不方便,必须有些核心功能无法使用,此消息的发布很有可能将这种格局打破。

本篇教程将介绍使用github的系列产品来实现项目的发布,构建,测试和部署,当然这仅仅是一个非常小的示例,有些地方后期可能会有更好的瞿恩方案。

GitHub Actions 是一款持续集成工具,包括clone代码,代码构建,程序测试和项目发布等一系列操作。更多内容参考:http://www.ruanyifeng.com/blog/2019/09/getting-started-with-github-actions.html

如果你对CI/CD不了解的话,建议先找些文档看看。

项目源文件见 https://github.com/cfanbo/github-actions-demo

GitHub Actions 术语

GitHub Actions 相关的术语。

(1)workflow (工作流程):持续集成一次运行的过程,就是一个 workflow。

(2)job (任务):一个 workflow 由一个或多个 jobs 构成,含义是一次持续集成的运行,可以完成多个任务。

(3)step(步骤):每个 job 由多个 step 构成,一步步完成。

(4)action (动作):每个 step 可以依次执行一个或多个命令(action)。

一、创建workflow 文件

在项目里创建一个workflow文件,文件格式为yaml类型。文件名可以随意起,文件后缀可以为yml 或 .yaml, 这里我们创建文件 .github/workflows/deploy.yaml,注意这里的路径。

如果你的仓库中有项目文件的话,当你点击“Actions”时,系统会自动根据你的开发语言推荐一个常用的actions,在页面的右侧也会推荐一些相应的actions.

二、在部署服务器上生成部署用户密钥

部署时需要用到用户的私钥,所以先登录到部署服务器获取私钥,这里为了方便,单独创建了一对公钥和公钥,

$ cd ~/.ssh
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): /root/.ssh/id_rsa_actions
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa_actions.****
Your public key has been saved in /root/.ssh/id_rsa_actions.pub.
The key fingerprint is:
SHA256:QPg26V17/pnRdHZrDqysG6jFgdTEysbz+aCuXusyO/Q root@iZbp1acq02ar70gdvppgh6Z
The key’s randomart image is:
+—[RSA 2048]—-+
| .o. |
| ..o. |****
| oooo |
| .**. . |
| .+o+S. . =|
| . o++ . o ++|
| . …+o. o o.o.|
| +.E+ .o o ++ |
| .+O= ooo .+. |
+—-[SHA256]—–+

这里为部署单独生成了一对公钥和私钥,私钥路径为 /root/.ssh/id_rsa_actions

将公钥保存到 authorized_keys 文件中
$ cat id_rsa_actions.pub >> authorized_keys

查看私钥内容,后面需要用到,先将内容保存起来
$ cat id_rsa_actions

三、准备工作

对于服务的部署所以这里选择了 ssh-deploy 这一个actioins,官方网址 https://github.com/marketplace/actions/ssh-deploy。

下面开始添加deploy.yaml文件中用到了一些变量。

在项目首页右上角点击 Seetings->Secets, 找到 Add a new secret,分别添加以下变量

SERVER_SSH_KEY 登录私钥,就是上面保存的私钥内容
REMOTE_HOST 服务器地址,如202.102.224.68
REMOTE_PORT (可选项)服务器ssh端口,一般默认为22
REMOTE_USER 登录服务器用户名,这时指密钥所属的用户
SOURCE (可选项),默认为‘’, 构建服务器路径,这里为相应 $GITHUB_WORKSPACE 根目录而言的相对路径, 例如 dist/
REMOTE_TARGET 服务器部署路径,如 /data/ghactions
ARGS (可选项)默认值为 -rltgoDzvO

四、上传代码到github远程仓库

我们这里定义了当master分支发生push操作时就触发一系列workflow操作。
$git push origin master
这时我们可以在 https://github.com/cfanbo/github-actions-demo/actions 页面看到当前项目的构建情况。

四、测试

这里我们登录到远程服务器,可以发现一个server可执行文件,表示已经成功部署到生产服务器了

五、其它

有关 workflow 的一些环境变量可参考 https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables

如果你有过CI/CD的经历,会发现本教程虽然实现了最为初级的功能,离真正线上使用还有一定的距离。主要存在以下问题:
1. 教程里只用了一个job,如果你的项目允许的话,完全可以利用多个job来同时异步构建。
2. 这里构建、测试和部署同时放在了一个job里,也不是太优雅
3. 这里的部署只是将最终生成的二进制文件上传到了生产服务器,并没有对服务进行启动操作或者说没有进行服务的热更新,这在一般场景下是不允许的
由于本篇文章只是一个简单介绍actions基本用法的教程,所以如果想真正用到工作中的话,可能还需要对李篇内容再完善完善才行。

参考

  • https://help.github.com/en/articles/workflow-syntax-for-github-actions
  • https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables
  • https://github.com/marketplace/actions/checkout
  • https://github.com/marketplace/actions/setup-go-for-use-with-actions
  • https://github.com/marketplace/actions/ssh-deploy
  • http://www.ruanyifeng.com/blog/2019/09/getting-started-with-github-actions.html

k8s中的Service与Ingress

集群中的服务要想向外提供服务,就不得不提到Service和Ingress。 下面我们就介绍一下两者的区别和关系。

Service

必须了解的一点是 Service 的访问信息在 Kubernetes 集群内是有效的,集群之外是无效的。

Service可以看作是一组提供相同服务的Pod对外的访问接口。借助Service,应用可以方便地实现服务发现和负载均衡。对于Service 的工作原理请参考https://time.geekbang.org/column/article/68636

当需要从集群外部访问k8s里的服务的时候,方式有四种:ClusterIP、NodePort、LoadBalancer、ExternalName 。

下面我们介绍一下这几种方式的区别

一、ClusterIP

通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的 ServiceType。

我们先看一下最简单的Service定义

apiVersion: v1
kind: Service
metadata:
  name: hostnames
spec:
  selector:
    app: hostnames
  ports:
  - name: default
    protocol: TCP
    port: 80
    targetPort: 9376

这里我使用了 selector 字段来声明这个 Service 只代理携带了 app=hostnames 标签的 Pod。并且这个 Service 的 80 端口,代理的是 Pod 的 9376 端口。

然后我们再看一下 Deployment 的定义

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hostnames
spec:
  selector:
    matchLabels:
      app: hostnames
  replicas: 3
  template:
    metadata:
      labels:
        app: hostnames
    spec:
      containers:
      - name: hostnames
        image: k8s.gcr.io/serve_hostname
        ports:
        - containerPort: 9376
          protocol: TCP

我们这里声明的Pod份数是3份,使用的镜像为 k8s.gcr.io/serve_hostname,主要提供输出hostname的功能,如果我们依次访问 curl 10.0.1.175:80 的话,会发现每次响应的内容,这是因为 Service 提供的是 Round Robin 方式的负载均衡。这个10.0.1.175是集群的IP地址,俗称VIP,是 Kubernetes 自动为 Service 分配的。对于这种方式,我们称为:ClusterIP 模式的 Service。

二、NodePort

通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 NodeIP:Port,可以从集群的外部访问一个 NodePort 服务。

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  type: NodePort
  ports:
  - nodePort: 8080
    targetPort: 80
    protocol: TCP
    name: http
  - nodePort: 443
    protocol: TCP
    name: https
  selector:
    run: my-nginx

Service描述文件:
spec.type 声明Service的type
spec.selector 字段确定这个Service将要使用哪些Label。在本例中,会管理所有具有 run: my-nginx 标签的Pod。
spec.ports.nodePort 表明此Service将会监听8080端口,并将所有监听到的请求转发给其管理的Pod。
spec.ports.targetPort 表明此Service监听到的8080端口的请求都会被转发给其管理的Pod的80端口。此字段可以省略,省略后其值会被设置为spec.ports.port的值。

如果你未指定 spec.ports.nodePort 的话,则系统会随机选择一个范围为 30000-32767的端口号使用。

这时要访问这个Service的话,只需要通过访问

<任何一台宿主机器的IP>:8080

这样可以访问到某一个被代理Pod的80端口。后端的Pod并不是一定在当前Node上,有可能你访问的Node1:8080,而后端对应的Pod是在Node2上。
可以看到这种方式很好理解,非常类似于平时我们使用docker部署容器应用后,将容器服务端口暴露出来宿主端口就可以了。

三、LoadBalancer

在NodePort基础上,Kubernetes可以请求底层云平台cloud provider 创建一个外部的负载均衡器,并将请求转发到每个Node作为后端,进行服务分发。
该模式需要底层云平台(例如GCE、亚马孙AWS)支持。

---
kind: Service
apiVersion: v1
metadata:
  name: example-service
spec:
  ports:
  - port: 8765
    targetPort: 9376
  selector:
    app: example
  type: LoadBalancer

创建这个服务后,系统会自动创建一个外部负载均衡器,其端口为8765, 并且把被代理的 地址添加到公有云的负载均衡当中。

四、ExternalName

创建一个dns别名指到service name上,主要是防止service name发生变化,要配合dns插件使用。

通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如 foo.bar.example.com)。
没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。

kind: Service
 apiVersion: v1
 metadata:
   name: my-service
 spec:
   type: ExternalName
   externalName: my.database.example.com

指定了一个 externalName=my.database.example.com 的字段。而且你应该会注意到,这个 YAML 文件里不需要指定 selector。
这时候,当你通过 Service 的 DNS 名字访问它的时候,比如访问:my-service.default.svc.cluster.local。
那么,Kubernetes 为你返回的就是my.database.example.com。所以说,ExternalName 类型的 Service,其实是在 kube-dns 里为你添加了一条 CNAME 记录。
这时访问 my-service.default.svc.cluster.local 就和访问 my.database.example.com 这个域名是一个效果了。

Ingress

上面我们提到有一个叫作 LoadBalancer 类型的 Service,它会为你在 Cloud Provider(比如:Google Cloud 或者 OpenStack)里创建一个与该 Service 对应的负载均衡服务。但是,相信你也应该能感受到,由于每个 Service 都要有一个负载均衡服务,所以这个做法实际上既浪费成本又高。作为用户,我其实更希望看到 Kubernetes 为我内置一个全局的负载均衡器。然后,通过我访问的 URL,把请求转发给不同的后端 Service。这种全局的、为了代理不同后端 Service 而设置的负载均衡服务,就是 Kubernetes 里的 Ingress 服务。

Ingress 的功能其实很容易理解:所谓 Ingress 就是 Service 的“Service”,这就是它们两者的关系。

    internet
        |
   [ Ingress ]
   --|-----|--
   [ Services ]

通过使用 Kubernetes 的 Ingress 来创建一个统一的负载均衡器,从而实现当用户访问不同的域名时,访问后端不同的服务。

假如我现在有这样一个站点:https://cafe.example.com。其中 https://cafe.example.com/coffee,对应的是“咖啡点餐系统”。而 https://cafe.example.com/tea,对应的则是“茶水点餐系统”。这两个系统,分别由名叫 coffee 和 tea 这样两个 Deployment 来提供服务。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: cafe-ingress
spec:
  tls:
  - hosts:
    - cafe.example.com
    secretName: cafe-secret
  rules:
  - host: cafe.example.com
    http:
      paths:
      - path: /tea
        backend:
          serviceName: tea-svc
          servicePort: 80
      - path: /coffee
        backend:
          serviceName: coffee-svc
          servicePort: 80

最值得我们关注的,是 rules 字段。在 Kubernetes 里,这个字段叫作:IngressRule。
IngressRule 的 Key,就叫做:host。它必须是一个标准的域名格式(Fully Qualified Domain Name)的字符串,而不能是 IP 地址。

而接下来 IngressRule 规则的定义,则依赖于 path 字段。你可以简单地理解为,这里的每一个 path 都对应一个后端 Service。
所以在我们的例子里,我定义了两个 path,它们分别对应 coffee 和 tea 这两个 Deployment 的 Service(即:coffee-svc 和 tea-svc)。

通过上面的介绍,不难看到所谓 Ingress 对象,其实就是 Kubernetes 项目对“反向代理”的一种抽象。

一个 Ingress 对象的主要内容,实际上就是一个“反向代理”服务(比如:Nginx)的配置文件的描述。而这个代理服务对应的转发规则,就是 IngressRule。

这就是为什么在每条 IngressRule 里,需要有一个 host 字段来作为这条 IngressRule 的入口,然后还需要有一系列 path 字段来声明具体的转发策略。这其实跟 Nginx、HAproxy 等项目的配置文件的写法是一致的。

在实际使用中,我们一般选择一种 Ingress Controller,将期部署在k8s集群中,这样它就会根据我们定义的 Ingress 对象来提供对应的代理功能。

业界常用的各种反向代理项目,比如 Nginx、HAProxy、Envoy、Traefik 等,都已经为 Kubernetes 专门维护了对应的 Ingress Controller。

Nginx Ingress Controller 的示例请参考 https://time.geekbang.org/column/article/69214

推荐参考官方推荐脚本:https://github.com/resouer/kubernetes-ingress/tree/master/examples/complete-example

mac下利用minikube安装Kubernetes环境

本机为mac环境,安装有brew工具,所以为了方便这里直接使用brew来安装minikube工具。同时本机已经安装过VirtualBox虚拟机软件。

minikube是一款专门用来创建k8s 集群的工具。

一、安装minikube

参考 https://kubernetes.io/docs/tasks/tools/install-minikube/, 在安装minkube之前建议先了解一下minikube需要的环境https://kubernetes.io/docs/setup/learning-environment/minikube/

1. 先安装一个虚拟化管理系统,如果还未安装,则在 HyperKit、VirtualBox 或 VMware Fusion 三个中任选一个即可,这里我选择了VirtualBox。

如果你想使用hyperkit的话,可以直接执行 brew install hyperkit 即可。

对于支持的driver_name有效值参考https://kubernetes.io/docs/setup/learning-environment/minikube/#specifying-the-vm-driver, 目前docker尚处于实现阶段。

$ brew install minikube

查看版本号

$ minikube version

minikube version: v1.8.2
commit: eb13446e786c9ef70cb0a9f85a633194e62396a1

安装kubectl命令行工具

$ brew install kubectl

二、启动minikube 创建集群

$ minikube start --driver=virtualbox

如果国内的用户安装时提示失败”VM is unable to access k8s.gcr.io, you may need to configure a proxy or set –image-repository”,
则指定参数–image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers

 $ minikube start --driver=virtualbox --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers

😄 minikube v1.8.2 on Darwin 10.15.3
✨ Using the virtualbox driver based on existing profile
✅ Using image repository registry.cn-hangzhou.aliyuncs.com/google_containers
💾 Downloading preloaded images tarball for k8s v1.17.3 …
⌛ Reconfiguring existing host …
🏃 Using the running virtualbox “minikube” VM …
🐳 Preparing Kubernetes v1.17.3 on Docker 19.03.6 …
> kubelet.sha256: 65 B / 65 B [————————–] 100.00% ? p/s 0s
> kubeadm.sha256: 65 B / 65 B [————————–] 100.00% ? p/s 0s
> kubectl.sha256: 65 B / 65 B [————————–] 100.00% ? p/s 0s
> kubeadm: 37.52 MiB / 37.52 MiB [—————] 100.00% 1.01 MiB p/s 37s
> kubelet: 106.42 MiB / 106.42 MiB [————-] 100.00% 2.65 MiB p/s 40s
> kubectl: 41.48 MiB / 41.48 MiB [—————] 100.00% 1.06 MiB p/s 40s
🚀 Launching Kubernetes …
🌟 Enabling addons: default-storageclass, storage-provisioner
🏄 Done! kubectl is now configured to use “minikube”

另外在minikube start 有一个选项是–image-mirror-country=’cn’  这个选项是专门为中国准备的,还有参数–iso-url,官方文档中已经提供了阿里云的地址……… 这个选项会让你使用阿里云的镜像仓库,我这里直接指定了镜像地址。

对于大部分国内无法访问到的镜像k8s.gcr.io域名下的镜像都可以在http://registry.cn-hangzhou.aliyuncs.com/google_containers 找到。

查看集群状态

$ minikube status

如果输出结果如下,则表示安装成功
host: Running
kubelet: Running
apiserver: Running
kubeconfig: Configured

如果你以前安装过minikube,在 minikube start 的时候返回错误 machine does not exist,则需要执行 minikube delete 清理本地状态。

这时创建完成后,会在VirtualBox软件里出现一个虚拟系统。

另外minikube 也支持 –driver=none参数

三、进阶篇

参考 https://kubernetes.io/docs/tutorials/hello-minikube/#before-you-begin

打开Kubernetes仪表板

$ minikube dashboard

🔌 Enabling dashboard …
🤔 Verifying dashboard health …
🚀 Launching proxy …
🤔 Verifying proxy health …
🎉 Opening http://127.0.0.1:59915/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser…

在浏览器中自动打开上方网址, 就可以看到 k8s 控制台。同时也可以根据上方网址教程来学习。

具体的操作,你可以查看 Dashboard 项目的官方文档

四、停止集群

$ minikube stop

五、删除集群

$ minikube delete

六、升级集群

$ brew update 
& brew upgrade minikube

mac 的请参考 https://minikube.sigs.k8s.io/docs/start/macos/#upgrading-minikube

参考资料

https://kubernetes.io/docs/setup/learning-environment/minikube/
https://kubernetes.io/docs/tasks/tools/install-minikube/
https://kubernetes.io/docs/tutorials/hello-minikube/#before-you-begin

Golang中的限速器 time/rate

在高并发的系统中,限流已作为必不可少的功能,而常见的限流算法有:计数器、滑动窗口、令牌桶、漏斗(漏桶)。其中滑动窗口算法、令牌桶和漏斗算法应用最为广泛。

常见限流算法

这里不再对计数器算法和滑动窗口作介绍了,有兴趣的同学可以参考其它相关文章。

漏斗算法

非常很好理解,就像有一个漏斗容器一样,漏斗上面一直往容器里倒水(请求),漏斗下方以固定速率一直流出(消费)。如果漏斗容器满的情况下,再倒入的水就会溢出,此时表示新的请求将被丢弃。可以看到这种算法在应对大的突发流量时,会造成部分请求弃用丢失。

可以看出漏斗算法能强行限制数据的传输速率。

漏斗算法

令牌桶算法

从某种意义上来说,令牌算法是对漏斗算法的一种改进。对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发情况。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。

令牌桶算法是指一个固定大小的桶,可以存放的令牌的最大个数也是固定的。此算法以一种固定速率不断的往桶中存放令牌,而每次请求调用前必须先从桶中获取令牌才可以。否则进行拒绝或等待,直到获取到有效令牌为止。如果桶内的令牌数量已达到桶的最大允许上限的话,则丢弃令牌。

Golang中的限制算法

Golang标准库中的限制算法是基于令牌桶算法(Token Bucket) 实现的,库名为golang.org/x/time/rate

对于限流器的消费方式有三种,分别为 Allow()、 Wait()和 Reserve()。前两种内部调用的都是Reserve() ,每个都对应一个XXXN()的方法。如Allow()是AllowN(t, 1)的简写方式。

结构体

type Limiter struct {
	limit Limit
	burst int

	mu     sync.Mutex
	tokens float64
	// last is the last time the limiter's tokens field was updated
	last time.Time
	// lastEvent is the latest time of a rate-limited event (past or future)
	lastEvent time.Time
}

主要用来限速控制并发事件,采用令牌池算法实现。

创建限速器

使用 NewLimiter(r Limit, b int) 函数创建限速器,令牌桶容量为b。初始化状态下桶是满的,即桶里装有b 个令牌,以后再以每秒往里面填充 r 个令牌。

func NewLimiter(r Limit, b int) *Limiter {
	return &amp;Limiter{
		limit: r,
		burst: b,
	}
}

允许声明容量为0的限速器,此时将以拒绝所有事件操作。

// As a special case, if r == Inf (the infinite rate), b is ignored.
有一种特殊情况,就是 r == Inf 时,此时b参数将被忽略。

// Inf is the infinite rate limit; it allows all events (even if burst is zero).
const Inf = Limit(math.MaxFloat64)

Limiter 提供了三个主要函数 Allow, Reserve, 和 Wait. 大部分时候使用Wait。其中 AllowN, ReserveN 和 WaitN 允许消费n个令牌。

每个方法都可以消费一个令牌,当没有可用令牌时,三个方法的处理方式不一样

  • 如果没有令牌时,Allow 返回 false。
  • 如果没有令牌时,Wait 会阻塞走到有令牌可用或者超时取消(context.Context)。
  • 如果没有令牌时,Reserve 返回一个 reservation,以便token的预订时,调用之前必须等待一段时间。

1. Allow/AllowN

AllowN方法表示,截止在某一时刻,目前桶中数目是否至少为n个。如果条件满足,则从桶中消费n个token,同时返回true。反之不消费Token,返回false。

使用场景:一般用在如果请求速率过快,直接拒绝请求的情况

package main

import (
	"context"
	"fmt"
	"time"

	"golang.org/x/time/rate"
)

func main() {
	// 初始化一个限速器,每秒产生10个令牌,桶的大小为100个
	// 初始化状态桶是满的
	var limiter = rate.NewLimiter(10, 100)

	for i := 0; i < 20; i++ {
		if limiter.AllowN(time.Now(), 25) {
			fmt.Printf("%03d Ok  %s\n", i, time.Now().Format("2006-01-02 15:04:05.000"))
		} else {
			fmt.Printf("%03d Err %s\n", i, time.Now().Format("2006-01-02 15:04:05.000"))
		}
		time.Sleep(500 * time.Millisecond)
	}

}

输出

000 Ok  2020-03-27 16:17:18.604
001 Ok  2020-03-27 16:17:19.110
002 Ok  2020-03-27 16:17:19.612
003 Ok  2020-03-27 16:17:20.115
004 Err 2020-03-27 16:17:20.620
005 Ok  2020-03-27 16:17:21.121
006 Err 2020-03-27 16:17:21.626
007 Err 2020-03-27 16:17:22.127
008 Err 2020-03-27 16:17:22.632
009 Err 2020-03-27 16:17:23.133
010 Ok  2020-03-27 16:17:23.636
011 Err 2020-03-27 16:17:24.138
012 Err 2020-03-27 16:17:24.642
013 Err 2020-03-27 16:17:25.143
014 Err 2020-03-27 16:17:25.644
015 Ok  2020-03-27 16:17:26.147
016 Err 2020-03-27 16:17:26.649
017 Err 2020-03-27 16:17:27.152
018 Err 2020-03-27 16:17:27.653
019 Err 2020-03-27 16:17:28.156

2. Wait/WaitN

当使用Wait方法消费Token时,如果此时桶内Token数量不足(小于N),那么Wait方法将会阻塞一段时间,直至Token满足条件。否则直接返回。
// 可以看到Wait方法有一个context参数。我们可以设置context的Deadline或者Timeout,来决定此次Wait的最长时间。

func main() {
	// 指定令牌桶大小为5,每秒补充3个令牌
	limiter := rate.NewLimiter(3, 5)

	// 指定超时时间为5秒
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
	defer cancel()

	for i := 0; ; i++ {
		fmt.Printf("%03d %s\n", i, time.Now().Format("2006-01-02 15:04:05.000"))

		// 每次消费2个令牌
		err := limiter.WaitN(ctx, 2)
		if err != nil {
			fmt.Printf("timeout: %s\n", err.Error())
			return
		}
	}

	fmt.Println("main")
}

输出

000 2020-03-27 16:53:34.764
001 2020-03-27 16:53:34.764
002 2020-03-27 16:53:34.764
003 2020-03-27 16:53:35.100
004 2020-03-27 16:53:35.766
005 2020-03-27 16:53:36.434
006 2020-03-27 16:53:37.101
007 2020-03-27 16:53:37.770
008 2020-03-27 16:53:38.437
009 2020-03-27 16:53:39.101
timeout: rate: Wait(n=2) would exceed context deadline

3. Reserve/ReserveN

// 此方法有一点复杂,它返回的是一个*Reservation类型,后续操作主要针对的全是这个类型
// 判断限制器是否能够在指定时间提供指定N个请求令牌。
// 如果Reservation.OK()为true,则表示需要等待一段时间才可以提供,其中Reservation.Delay()返回需要的延时时间。
// 如果Reservation.OK()为false,则Delay返回InfDuration, 此时不想等待的话,可以调用 Cancel()取消此次操作并归还使用的token

func main() {
	// 指定令牌桶大小为5,每秒补充3个令牌
	limiter := rate.NewLimiter(3, 5)

	// 指定超时时间为5秒
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
	defer cancel()
	for i := 0; ; i++ {
		fmt.Printf("%03d %s\n", i, time.Now().Format("2006-01-02 15:04:05.000"))
		reserve := limiter.Reserve()
		if !reserve.OK() {
			//返回是异常的,不能正常使用
			fmt.Println("Not allowed to act! Did you remember to set lim.burst to be > 0 ?")
			return
		}
		delayD := reserve.Delay()
		fmt.Println("sleep delay ", delayD)
		time.Sleep(delayD)
		select {
		case <-ctx.Done():
			fmt.Println("timeout, quit")
			return
		default:
		}
		//TODO 业务逻辑
	}

	fmt.Println("main")
}

输出

000 2020-03-27 16:57:23.135
sleep delay  0s
001 2020-03-27 16:57:23.135
sleep delay  0s
002 2020-03-27 16:57:23.135
sleep delay  0s
003 2020-03-27 16:57:23.135
sleep delay  0s
004 2020-03-27 16:57:23.135
sleep delay  0s
005 2020-03-27 16:57:23.135
sleep delay  333.292866ms
006 2020-03-27 16:57:23.474
sleep delay  328.197741ms
007 2020-03-27 16:57:23.804
sleep delay  331.211817ms
008 2020-03-27 16:57:24.136
sleep delay  332.779335ms
009 2020-03-27 16:57:24.473
sleep delay  328.952586ms
010 2020-03-27 16:57:24.806
sleep delay  329.620588ms
011 2020-03-27 16:57:25.136
sleep delay  332.404798ms
012 2020-03-27 16:57:25.474
sleep delay  328.456103ms
013 2020-03-27 16:57:25.803
sleep delay  331.34754ms
014 2020-03-27 16:57:26.136
sleep delay  332.285545ms
015 2020-03-27 16:57:26.473
sleep delay  328.673618ms
016 2020-03-27 16:57:26.803
sleep delay  332.296438ms
017 2020-03-27 16:57:27.137
sleep delay  332.201646ms
018 2020-03-27 16:57:27.474
sleep delay  328.312813ms
019 2020-03-27 16:57:27.803
sleep delay  332.210098ms
020 2020-03-27 16:57:28.136
sleep delay  332.854719ms
timeout, quit

参考资料

https://www.cyhone.com/articles/analisys-of-golang-rate/
https://zhuanlan.zhihu.com/p/100594314
https://www.jianshu.com/p/1ecb513f7632
https://studygolang.com/articles/10148

Golang中的两个定时器 ticker 和 timer

Golang中time包有两个定时器,分别为ticker 和 timer。两者都可以实现定时功能,但各自都有自己的使用场景。

区别

  • ticker定时器表示每隔一段时间就执行一次,一般可执行多次。
  • timer定时器表示在一段时间后执行,默认情况下只执行一次,如果想再次执行的话,每次都需要调用 time.Reset()方法,此时效果类似ticker定时器。同时也可以调用stop()方法取消定时器
  • timer定时器比ticker定时器多一个Reset()方法,两者都有Stop()方法,表示停止定时器,底层都调用了stopTimer()函数。

Ticker定时器

package main

import (
	"fmt"
	"time"
)

func main() {
    // Ticker 包含一个通道字段C,每隔时间段 d 就向该通道发送当时系统时间。
    // 它会调整时间间隔或者丢弃 tick 信息以适应反应慢的接收者。
    // 如果d <= 0会触发panic。关闭该 Ticker 可以释放相关资源。

	ticker1 := time.NewTicker(5 * time.Second)
	// 一定要调用Stop(),回收资源
	defer ticker1.Stop()
	go func(t *time.Ticker) {
		for {
			// 每5秒中从chan t.C 中读取一次
			<-t.C
			fmt.Println("Ticker:", time.Now().Format("2006-01-02 15:04:05"))
		}
	}(ticker1)

	time.Sleep(30 * time.Second)
	fmt.Println("ok")
}

执行结果

开始时间: 2020-03-19 17:49:41
Ticker: 2020-03-19 17:49:46
Ticker: 2020-03-19 17:49:51
Ticker: 2020-03-19 17:49:56
Ticker: 2020-03-19 17:50:01
Ticker: 2020-03-19 17:50:06
结束时间: 2020-03-19 17:50:11
ok

可以看到每次执行的时间间隔都是一样的。

Timer定时器

package main

import (
	"fmt"
	"time"
)

func main() {

	// NewTimer 创建一个 Timer,它会在最少过去时间段 d 后到期,向其自身的 C 字段发送当时的时间
	timer1 := time.NewTimer(5 * time.Second)

	fmt.Println("开始时间:", time.Now().Format("2006-01-02 15:04:05"))
	go func(t *time.Timer) {
		times := 0
		for {
			<-t.C
			fmt.Println("timer", time.Now().Format("2006-01-02 15:04:05"))

			// 从t.C中获取数据,此时time.Timer定时器结束。如果想再次调用定时器,只能通过调用 Reset() 函数来执行
			// Reset 使 t 重新开始计时,(本方法返回后再)等待时间段 d 过去后到期。
			// 如果调用时 t 还在等待中会返回真;如果 t已经到期或者被停止了会返回假。
			times++
			// 调用 reset 重发数据到chan C
			fmt.Println("调用 reset 重新设置一次timer定时器,并将时间修改为2秒")
			t.Reset(2 * time.Second)
			if times > 3 {
				fmt.Println("调用 stop 停止定时器")
				t.Stop()
			}
		}
	}(timer1)

	time.Sleep(30 * time.Second)
	fmt.Println("结束时间:", time.Now().Format("2006-01-02 15:04:05"))
	fmt.Println("ok")
}

执行结果

开始时间: 2020-03-19 17:41:59
timer 2020-03-19 17:42:04
调用 reset 重新设置一次timer定时器,并将时间修改为2秒
timer 2020-03-19 17:42:06
调用 reset 重新设置一次timer定时器,并将时间修改为2秒
timer 2020-03-19 17:42:08
调用 reset 重新设置一次timer定时器,并将时间修改为2秒
timer 2020-03-19 17:42:10
调用 reset 重新设置一次timer定时器,并将时间修改为2秒
调用 stop 停止定时器
结束时间: 2020-03-19 17:42:29
ok

可以看到,第一次执行时间为5秒以后。然后通过调用 time.Reset() 方法再次激活定时器,定时时间为2秒,最后通过调用time.Stop()把前面的定时器取消掉

注意事项

1. 这里需要注意的时,如果在调用 time.Reset() 或time.Stop() 的时候,timer已经过期或者停止了,则会返回false。


func main() {
	// timer 过期
	timer := time.NewTimer(2 * time.Second)
	time.Sleep(3 * time.Second)
	ret := timer.Reset(2 * time.Second)
	fmt.Println(ret)

	// timer 停止
	timer = time.NewTimer(2 * time.Second)
	timer.Stop()
	ret = timer.Reset(2 * time.Second)
	fmt.Println(ret)

	fmt.Println("ok")
}

执行结果

false
false
ok

2. 如果调用 time.Stop() 时,timer已过期或已stop,则并不会关闭通道。

3. 使用time.NewTicker() 定时器时,需要使用Stop()方法进行资源释放,否则会产生内存泄漏,(Stop the ticker to release associated resources.)