404频道

学习笔记

第三天

行程:宁夏回族自治区中卫市中宁县 -> 青海省西宁市

路程:500公里

日期:2019.4.20

昨晚到酒店比较晚,早上拉开窗帘,一所学校的操场映入眼帘,令我眼前一亮。离开校园已经太久了,看到同学们身穿运动装在操场上洋溢着青春即亲切又陌生。

第一站 黄河畔

从地图上看中宁县城离黄河比较近,自然要去黄河边上走一走。但与保德县和府谷县不同的是,中宁县并不傍黄河而建,主城区而是离黄河有一公里的距离。我猜测保德县和府谷县边的黄河在地势低洼处,即使河水泛滥也不会殃及县城。但中宁县地势却是非常平坦,不能依靠地势优势阻止泛滥的河水,因此县城的选址离黄河稍有距离。

远处的黄河边上有一个多层的塔,是中宁县枸杞博物馆,中宁县有“天下枸杞出宁夏,中宁枸杞甲天下”的说法,因此建个枸杞博物馆还是比较合适的。由于到枸杞博物馆的路并不好走,而且不能判断此刻博物馆是对外开放,并未前往参观。

黄河水比其下游的府谷县的黄河水要变黄了很多,这也是我一直比较奇怪的地方。河道非常宽,目测有至少1公里的样子,现在是枯水期,河水并不太多。河床上有各式各样的鹅卵石,不知经过了多少次的河水冲刷才形成了现在的形状,我们个体的生命相对鹅卵石的生命实在是太短,挑选了几个像模像样的留作纪念。

在路上

宁夏境内,一路上偶尔会看到种植了一些铺着地膜的农作物,我猜测是土豆的可能性比较大,原因是山东的种植土豆方法就是类似思路。但看下田地中的土质,大部分都是小石块,哪有多少土壤成分呀,倒是拿着石块压着地膜来防止大风非常的方便。

在甘肃和青海接壤地方,道路两侧多了很多树木,,山上的草有很多都绿了起来,生机勃勃的。两天之内满眼都是枯黄色的画面,突然出现鲜绿色还有点不适应。

查看青海省的地图会发现,有好几个地级市都是围绕着青海湖命名的,海东市、海南藏族自治州、海西蒙古族藏族自治州、海北藏族自治州。提到海南,不熟悉的人还以为是海南岛呢。

地图中还有一个比较有意思的地方,海西蒙古族藏族自治州居然有两块区域,中间被玉树给分割开了,完全不接壤。让我立马想到的是在中国版图中另外比较有意思的是河北省的三河市,三河市被北京市和天津市包围,像是河北省的一个孤岛。

青海省有藏族和回族比较容易理解,居然还有蒙古族?要不也就不会叫海西蒙古族藏族自治州了。实际上内蒙古的最西端跟青海省仅隔了甘肃省一段狭长的河西走廊,直线距离上还是特别近的。蒙古族迁徙到青海要从元朝说起,整个中国的版图都是元朝的天下,自然青海省有就了蒙古族,后续又陆陆续续的有蒙古族迁徙都青海省。但青海省的蒙古族还吸收了部分当地藏族的习俗,很难说是蒙古族入侵藏族,还是藏族同化了蒙古族。

青海境内的青藏高速施工,走了很长一段的非高速,跟青海的村庄和县城有了第一次近距离接触的机会。青海省的海东市相对于甘肃而言,植被还是比较茂盛的,树木挺多的,沿路很多梨树,开着洁白的梨花。经过村庄的房屋看起来也不差,很多都是二层的小楼,而且村庄还是比较密集的,大概青海人口都聚集在了东部地区。总体给我外观感受,海东地区要比甘肃东部地区富裕很多。

第二站 西宁市

西宁市属于中国的西部地区,如果放眼到中国的地图中,相同纬度上,西宁算是比较靠中间的城市了,如果不是西部人烟稀少,更像是个中部城市。

晚上八点钟到达西宁市,事先知道青海省的人口只有区区600万,省会应该不咋地,结果还挺另我刮目相看的,城区面积感官上还是挺大的,颇具省会城市的规模,各种品牌应有尽有,夜景也还不错。

晚上吃了一碗当地特色羊肠面,面相不咋地,吃起来味道还不错。

收个尾

1
2
3
4
5
6
人人都说宁夏美
砂石大漠看到尾
甘东地区很荒凉
似乎水源很紧张
夏都街头走一走
车水马龙大高楼

未完待续…

第二天

行程:山西省保德县 -> 陕西省榆林市府谷县 -> 宁夏回族自治区中宁县

路程:600公里

第一站 - 府谷县

第一天留宿的保德县临近黄河,早上醒来后本想去黄河边看下景色,其实最关心的是中段黄河的河水是什么颜色的,可惜黄河边是一条公路沿着黄河蜿蜒,而且局部在修,因为黄河的地势较低,视线受阻严重,连黄河都没看到。

索性直接导航到了黄河对岸的府谷县,山西省保德县跟陕西省榆林市府谷县隔黄河相望,仅一河之遥,可以在跨河大桥上非常方便的在两个省之间通行。跨河大桥并不是特别宽,两车会车是没任何问题的,桥上还有一些行人在通行。上图为连接两省的大桥,在府谷县可以看到“秦晋之好“几个大字。

保德县虽紧挨着黄河,却没有依靠上黄河的任何优势,反而黄河成为了一大劣势。相反,府谷县却非常明智的,楼房离着黄河稍有一段距离,选择在黄河边修建了滨河公园供休闲娱乐。

在府谷县的滨河公园可以清晰看到和对面的山西省保德县城,建筑物的朝向大多都是面向黄河,或者面向对面的府谷县城的。

由于现在是枯水期,人行道离河水还有一段距离,但仍可以看出黄河水还是特别清澈的,跟黄河的淤泥还是有比较鲜明的区别。跟更下游那夹杂的泥沙的黄河水一对比,让人完全联想不到是一条河。后面的行程中在更上游看到了几次黄河,但河水都要比此段的黄河水要黄,至少兰州的黄河水夹杂的泥沙量已经非常大,也就是说此段黄河水是我见到最清澈的。不知道是否我的判断有误,从理论上讲应该越下游黄河水越黄才对的。

在路上

下午一点从府谷县出发,直奔西宁方向而去,最终到达了宁夏回族自治州中卫市中宁县城,最初地貌还是黄土高坡,大概从榆林市之后逐渐沙漠化,地上草还没长出来,在高速上很难判断远处是草原,还是完全的荒漠。

上图为在高速上的简陋厕所,与其说是黄土高坡地貌,我感觉更像是荒漠。

本以为到达宁夏后,地貌会变成接近于草原,因为提到宁夏我最先想到的是黄河冲积而成的河套平原。一路上一直在期盼,但一直也没遇到梦中的草肥水美的河套平原。事后一查资料才发现,即使河套平原最南的“西套”地区也在银川市周边,而我走的高速在银川市的南边大约100多公里的距离,可以说是跟河套平原擦肩而过。从卫星地图上可以清晰的看出河套地区的植被还是比较茂密的。上图中绿色的线为我经过的路线。

终点站 - 中宁县

最终到达了宁夏回族自治州中卫市中宁县城,到达中宁县后,恰巧赶上了一次沙尘暴,停车吃饭的时候,车上已经积了厚厚的一层土。本以为沙尘暴在当地是家常便饭,问下了当地人,说是沙尘暴也不常有,沙尘是从北部的腾格里沙漠吹过来的。从上图中可以看出,中宁县离得腾格里沙漠还是特别近的,刮个北风,沙尘天气还是非常容易出现的。

比较巧合的是,中宁县也恰巧在黄河边上,一天之内,驱车600公里,从黄河“几”字形的右边到达了左边位置。

却道是

1
2
3
4
5
6
7
8
9
10
11
12
日行千余里
胜似的卢骋
朝食保德县
夕塌宁安镇
黄土变沙漠
高山转草原
早在黄河畔
夕亦黄河边
自东向西行
作何现此况
若君心有疑
一览地图觅

未完待续…

近期有一段较为充裕的假期,出去旅游对我而言是再合适不过的选择,带着对远方田野的那份向往。特别有意向的方向有两个:南下和西进。

南方的很多地方都未曾去过, 比如张家界、桂林等,可以从北京出发一路到海南三亚地区,而且当下正巧是烟花三月下江南的好季节。

对于西部地区向往已久,青海、西藏和新疆地区都未曾去过,当下四月份不是西部地区的最佳旅游季节,山基本上还都是枯黄色,草儿还在沉睡中。

但心想南方交通较为便利,后续会去的可能性比较大,而且景区之间跨度较大,可以分开旅游。而西部往往需要拿出一大把的时间来玩耍才能尽兴,很多地方的交通不是特别方便,往往都是需要多个景点一起才最划算。

因此,最终的决定是北京自驾甘肃-青海大环线

此刻的我已经结束了整个行程,行程的时间段为2019.4.18 - 2019.4.30,共计13天时间。起点为北京市,终点为山东省,整个的行程达6000公里。

事先计划的时间为12天,留了一天的buffer,最终用时为13天,没有太大的偏差。由于一直是我一人在开车,相对而言还是比较累的,基本上一天得开500公里,尤其是前面和后面赶路的时间,基本上一天在600公里以上。大部分时间是上午在10点之后出发,下午开车的时间比较多,很多时候需要晚上开一段时间才能满足一天的行程目标。如果时间比较充裕,把整个行程放慢为20天,感觉会比较轻松,会对旅行有更深的体会,只是我时间不太允许。

一路上经过北京、河北、山西、陕西、宁夏、甘肃、青海、河南、山东这些省,地貌包括平原、山脉、黄土高原、戈壁、沙漠、黄河、湖泊、雪山、盆地等地貌。

这一路上遇到的很多地方,之前在历史和地理课本上曾经了解过,很多地方有比较深厚的历史和文化底蕴,自己的认识却是很浅薄,而旅行可以加深对一个地方的地理和历史的认识,正所谓:“纸上得来终觉浅,绝知此事要躬行”。

本系列文章并不打算写成单纯游记的形式,而是更多会将我个人的体会和相关的历史地理知识融合在其中,在这其中查阅相关的资料对我而言也是一种成长。由于才疏学浅,所查资料有限,很多观点不见得是正确的,欢迎指正。

第一天

行程:北京市 -> 山西省忻州市保德县

上午11点从北京出发,驱车700公里,晚上八点半到达山西省忻州市保德县城。

先后经过了华北平原、太行山脉、黄土高原,北京和河北境内大部分属于华北平原,河北和山西省的交界处为太行山脉,期间穿过了多个隧道,基本上是整个的行程中走过的最长的一段隧道了,后续除了祁连山脉、六盘山外隧道就比较少了。

到了山西,以黄土高坡地貌为主,黄土高原也是整个行程中看到最多的地貌了。黄土高原东起太行山脉,西至青海省日月山,南抵秦岭,北达长城,包括了山西、陕西、宁夏、甘肃、青海、河南、内蒙古7个省。比我印象中的要大得多,我印象中主要是在山西境内和陕西的北部地区,一下子刷新了我的认知。

大家都知道,黄土高原是水土流失导致现在的千沟万壑的地貌,也是因此而导致黄河水特别的黄,那么是什么时间段发生的水土流失呢?历史上的黄土高原又是什么样的呢?

黄土高原最初是被植被茂密的森林所覆盖,而且土壤在有充足水源的情况下还是特别肥沃的,并非大家印象中的植被少的可怜,要不然也不可能孕育出华夏文明。相反,正是由于黄土土质疏松易开垦的特点,才容易诞生人类文明,因为最早的人类文明生产工具是特别简陋的,对大自然的改造能力特别有限。黄土高原被破坏的历史时期基本上伴随着华夏文明的发展,到明清时期达到顶峰,建国之后开始逐渐恢复。

很难想象,偌大的黄土高原,竟然因为农耕时代的人类影响变成了如今这幅模样。假如再给现代的人类一次利用自然的机会,我想当下的人类一定会善加利用这片广袤的土地,黄河也许会改成另外一个名字。也就是说,人类不同的文明程度决定了对自然的破坏程度。

但今天的我们殊不知也在对自然进行着更深层次的破坏,如果放到未来来审视我们人类今天的作为,我们今天对这个星球的破坏要大大高于农耕时代对地球的破坏。比如,人类在贪婪的消耗着地球上的石油、煤炭资源,也许在不久的未来,人类会发现在这些燃料资源中还蕴藏着无穷尽的价值,但是这些资源已经被当下愚蠢的人类当做燃料给烧掉了。

一路上雾霾还是比较严重的,尤其是河北保定境内。保定的空气质量是早有心理预期,印象中山西的整体空气还是非常不错的。去年去过一次五台山,穿过几个长长的隧道进入山西境内后,空气会一下子好转,山西省忻州市的空气跟河北省保定的空气质量有着强烈的反差。但这次结果却比较失望,在整个山西境内并未出现晴空万里,而且看上去更像是雾霾,而不是阴天。

山西省境内有那么几段高速,连一辆车都看不到,让我一度怀疑已经抵达大西北的感觉。

服务区已成为乌鸦的聚集地

从服务区鸟瞰村庄

晚上到达保德县,县城傍黄河而建,位于河道东侧。离黄河稍远的地方是黄土高坡,大部分楼房都离建在离黄河较劲的地势平坦处,而黄土高坡上由于地势的原因房屋逐渐变少。一进入县城,道路上到处都是开着远光灯的,仿佛对面一颗颗行进中的太阳,闪的我实在有些难受。有点匪夷所思,明明有路灯,实在想不明白远光灯的用途。真期望每个人作为社会中的一份子,在做事情的时候要多为他人考虑一分,而不是仅顾及自己的感受。

保德县城到处打着扫黑除恶的标语,吓得我开车都格外小心。甚至连小区门口都打着口号,让市民天天回家都可以看到。我深知越是提倡什么,说明越是缺少什么。后来走过好多地方后,发现到处都打着该口号,并不是当地特色。

却道是:

1
2
3
4
5
6
驱车青甘大环线
夜晚留宿保德县
县城建在黄河畔
黄土高坡把城建
黄河对岸府谷县
一桥之隔秦晋缘

未完待续…

最长公共子串及公共子序列问题属于一类问题,都可以使用动态规划的算法来解析,且动态规划方式比较类似,比较容易混淆。

定义

最长公共子串:两个字符串中,相同的最长子串,字符必须是连续的

最长公共子序列:两个字符串中,相同的最长序列,字符不一定是连续的

比如:a[] = “abcde” b[] = “bce”

那么:
最长子串:”bc”
最长子序列:”bce”

解法

假设A、B分别表示两个字符串

最长公共子串

dp[i][j]表示子串A[:i]、B[:j]必须以A[i]、B[j]为结尾的两个字符串的最大子串长度

1
2
3
4
5
if A[i] == B[j] {
dp[i][j] = dp[i-1][j-1]
} else {
dp[i][j] = 0
}

最终dp二维数组中的最大值即为结果

最长公共子序列

dp[i][j]表示子串A[:i]、B[:j]的两个字符串的最大子序列

1
2
3
4
5
if A[i] == B[j] {
dp[i][j] = dp[i-1][j-1]
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
}

最终dp[i-1][j-1]为结果

leetcode

今天发现vagrant的其中一个虚拟机磁盘空间不够了,需要对其进行磁盘扩容,但不期望是通过增加新硬盘的方式,而是直接增加原磁盘容量的方式来无缝扩容。

修改磁盘文件

进入到vm磁盘文件所在的目录~/VirtualBox VMs/dev_default_1531796361866_92956

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
% vboxmanage showhdinfo  centos-vm-disk1.vmdk
UUID: acbb4ffc-0580-40d6-8627-3ed24cd0beff
Parent UUID: base
State: created
Type: normal (base)
Location: /Users/lvkai/VirtualBox VMs/dev_default_1531796361866_92956/centos-vm-disk1.vmdk
Storage format: VMDK
Format variant: dynamic default
Capacity: 10000 MBytes
Size on disk: 9634 MBytes
Encryption: disabled
In use by VMs: dev_default_1531796361866_92956 (UUID: a153957c-e43f-4dd2-8512-f51d42dee3d3)

# 将之前存储的vmdk格式的文件复制一份vdi格式的文件,由于需要复制文件,该命令需要执行一段时间
% vboxmanage clonehd centos-vm-disk1.vmdk new-centos-vm-disk1.vdi --format vdi

# 将vdi格式的文件修改磁盘空间上限大小为80g,但实际占用磁盘空间仍然为之前的大小
% vboxmanage modifyhd new-centos-vm-disk1.vdi --resize 81920

# 将vdi格式的文件重新转换为vmdk格式,会产生一个新的uuid
% vboxmanage clonehd new-centos-vm-disk1.vdi resized.vmdk --format vmdk
0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%
Clone medium created in format 'vmdk'. UUID: 7e454b50-0681-494b-b9ca-81700d217c0a

新的硬盘创建完成后,在virtualbox的界面上将对应虚拟机的硬盘更换为resized.vmdk,并将之前旧的centos-vm-disk1.vmdk给删除掉。

使用fdisk创建新的磁盘分区

以上命令执行完成后,开启虚拟机,进入系统,可以看到磁盘空间大小变更为85.9GB,但挂载的磁盘空间大小仍然为8.3G,新增加的磁盘空间仍然处于未分配状态。

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
# fdisk -l

Disk /dev/sda: 85.9 GB, 85899345920 bytes, 167772160 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x0000ca5e

Device Boot Start End Blocks Id System
/dev/sda1 * 2048 1026047 512000 83 Linux
/dev/sda2 1026048 20479999 9726976 8e Linux LVM

Disk /dev/mapper/centos-root: 8866 MB, 8866758656 bytes, 17317888 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes


Disk /dev/mapper/centos-swap: 1048 MB, 1048576000 bytes, 2048000 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/centos-root 8.3G 7.9G 386M 96% /
devtmpfs 296M 0 296M 0% /dev
tmpfs 307M 0 307M 0% /dev/shm
tmpfs 307M 4.5M 303M 2% /run
tmpfs 307M 0 307M 0% /sys/fs/cgroup
/dev/sda1 497M 195M 303M 40% /boot
vagrant 466G 390G 77G 84% /vagrant
vagrant_data 466G 390G 77G 84% /vagrant_data
tmpfs 62M 0 62M 0% /run/user/1000

接下来需要将未分配的磁盘空间

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
# fdisk /dev/sda
# 依次输入可创建新的分区
n
p
回车
回车

# 继续输入p,可以看到磁盘的情况,多出了/dev/sda3
# /dev/sda3的System为Linux,而/dev/sda2的System为Linux LVM
Command (m for help): p

Disk /dev/sda: 85.9 GB, 85899345920 bytes, 167772160 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x0000ca5e

Device Boot Start End Blocks Id System
/dev/sda1 * 2048 1026047 512000 83 Linux
/dev/sda2 1026048 20479999 9726976 8e Linux LVM
/dev/sda3 20480000 167772159 73646080 83 Linux

# 依次输入将/dev/sda3更改为LVM格式
t
3
8e
p

Disk /dev/sda: 85.9 GB, 85899345920 bytes, 167772160 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x0000ca5e

Device Boot Start End Blocks Id System
/dev/sda1 * 2048 1026047 512000 83 Linux
/dev/sda2 1026048 20479999 9726976 8e Linux LVM
/dev/sda3 20480000 167772159 73646080 8e Linux LVM

# 输入w后进行保存操作
Command (m for help): w
The partition table has been altered!

Calling ioctl() to re-read partition table.

WARNING: Re-reading the partition table failed with error 16: Device or resource busy.
The kernel still uses the old table. The new table will be used at
the next reboot or after you run partprobe(8) or kpartx(8)
Syncing disks.

将新创建的磁盘分区添加到LVM分区中

将机器重启后,继续执行如下命令

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
# vgdisplay
--- Volume group ---
VG Name centos
System ID
Format lvm2
Metadata Areas 1
Metadata Sequence No 3
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 2
Open LV 2
Max PV 0
Cur PV 1
Act PV 1
VG Size 9.27 GiB
PE Size 4.00 MiB
Total PE 2374
Alloc PE / Size 2364 / 9.23 GiB
Free PE / Size 10 / 40.00 MiB
VG UUID cpEmYK-XFew-6ZWT-GEeY-yEou-0vLq-OJiD08

# lvscan
ACTIVE '/dev/centos/swap' [1000.00 MiB] inherit
ACTIVE '/dev/centos/root' [<8.26 GiB] inherit

# pvcreate /dev/sda3
Physical volume "/dev/sda3" successfully created.

# vgextend centos /dev/sda3
Volume group "centos" successfully extended

# lvextend /dev/centos/root /dev/sda3
Size of logical volume centos/root changed from <8.26 GiB (2114 extents) to <78.49 GiB (20093 extents).
Logical volume centos/root successfully resized.

# xfs_growfs /dev/centos/root
meta-data=/dev/mapper/centos-root isize=256 agcount=4, agsize=541184 blks
= sectsz=512 attr=2, projid32bit=1
= crc=0 finobt=0 spinodes=0
data = bsize=4096 blocks=2164736, imaxpct=25
= sunit=0 swidth=0 blks
naming =version 2 bsize=4096 ascii-ci=0 ftype=0
log =internal bsize=4096 blocks=2560, version=2
= sectsz=512 sunit=0 blks, lazy-count=1
realtime =none extsz=4096 blocks=0, rtextents=0
data blocks changed from 2164736 to 20575232

# 最后执行命令可以看到磁盘空间已经增加
# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/centos-root 79G 7.9G 71G 11% /
devtmpfs 296M 0 296M 0% /dev
tmpfs 307M 0 307M 0% /dev/shm
tmpfs 307M 4.5M 303M 2% /run
tmpfs 307M 0 307M 0% /sys/fs/cgroup
/dev/sda1 497M 195M 303M 40% /boot
vagrant 466G 390G 77G 84% /vagrant
vagrant_data 466G 390G 77G 84% /vagrant_data
tmpfs 62M 0 62M 0% /run/user/1000

常用垃圾回收方法

1.引用计数

类似于C++中的智能指针,每个对象维护一个引用计数,当对象被销毁时引用计数减一,当引用计数为0时立即回收对象。

在PHP、Python中使用,适用于内存比较紧张和实时性比较高的系统。

缺点:

  1. 由于频繁更新引用计数,降低了性能。
  2. 循环引用问题。解决办法为避免循环引用。

2.标记(mark)-清除(sweep)

分为标记和清除两个阶段,算法在70年代就提出了,非常古老的算法。

该算法有一个标记初始的root区域,和一个受控堆区。root区域主要是程序当前的栈和全局数据区域。

从root区域开始遍历所有被引用的对象,所有被访问到的对象被标记为“被引用”,标记完成后对未被引用的对象进行内存回收。

缺点:

标记阶段,都会Stop The World,会大大降低性能。

3.分代回收

将堆划分为两个或者多个代空间,新创建的对象存放于新生代,随着垃圾回收的重复执行,生命周期较长的对象会被放到老年代中。新生代和老年代采用不同的垃圾回收策略。

Go垃圾回收机制

Go中采用三色标记算法,本质上还是标记清除算法,但是对标记阶段有改进。

https://upload.wikimedia.org/wikipedia/commons/1/1d/Animation_of_tri-color_garbage_collection.gif

步骤如下:

  1. 开始时,所有的对象均为白色
  2. 从root开始扫描所有可达对象,标记为灰色,放入待处理队列。root包括了全局指针和goroutine栈上的指针。
  3. 从队列取出所有灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。
  4. 重复3,直到灰色队列为空。
  5. 将白色对象进行回收

优点:用户程序和标记操作可以并行执行。

详细过程如下图:

通过上图可以看出,STW有两个过程:

  1. gc开始的时候,需要一些准备工作,如开启write barrier
  2. re-scan的过程

每个对象需要一个标记位,go中并未将标记位存放于对象的内存区域中,而是采用非侵入式的标记位。go单独使用了标记位图区域来对应内存中的堆区域。

write barrier: 在运行垃圾回收算法的同时,应用程序在一直执行,上文中提到了写屏障write barrier用于将这些内存的操作记录下来。

GC日志

GC日志对于定位问题还是比较方便的

1.开启GC日志

可以增加环境变量GODEBUG=gctrace=1来开启gc日志,gc日志会打印到标准错误中。例如有如下的程序:

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

import "time"

func main() {
for {
s := make([]int, 10)
for i := 0; i < 10000; i++ {
s = append(s, i)
}
time.Sleep(time.Nanosecond)
}
}

执行GODEBUG=gctrace=1 go run gc.go即可打印gc日志到标准错误中。

2.GC日志的含义

可以参照runtime

1
2
3
4
5
6
7
8
9
gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P
where the fields are as follows:
gc # the GC number, incremented at each GC
@#s time in seconds since program start
#% percentage of time spent in GC since program start
#+...+# wall-clock/CPU times for the phases of the GC
#->#-># MB heap size at GC start, at GC end, and live heap
# MB goal goal heap size
# P number of processors used

例子:

gc 11557 @179.565s 2%: 0.018+1.2+0.049 ms clock, 0.14+1.1/2.3/0.90+0.39 ms cpu, 13->14->7 MB, 14 MB goal, 8 P

3.GC的监控

对于线上GC的监控,基本上读取runtime.MemStats结构中的内容,然后存储到时序数据库中。具体有如下两种获取方式:

1
2
3
4
5
6
// 方式1
memStats := &runtime.MemStats{}
runtime.ReadMemStats(memStats)

// 方式2 json格式
expvar.Get("memstats").String()

references

古语有云,一年之计在于春,这句话对于很多植物而言再合适不过。在经历了寒冬之后,很多植物选择将最美好的一面在春天里绽放。

资源

1.PagerDuty

PagerDuty是一家Sass平台厂商,其产品为一款告警处理平台,提供了On-Call管理、告警收敛分组、告警时间报表,并集成了多种告警方式。

2.Fathom

https://github.com/usefathom/fathom/raw/master/assets/src/img/fathom.jpg?v=7

一款开源的简易网站数据分析工具,类似于Google Analytics或者百度分析。

3.Data Structure Visualizations

该网站将常见的计算机数据结构以可视化的形式展示在了界面上,可以在界面上点击按钮完成插入元素、删除元素等操作,对应的数据结构展现会实时发生变化,非常直观。

4.snowflake

https://user-gold-cdn.xitu.io/2018/2/11/16182507bcefae54?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

Twitter开源的分布式算法,常用于分布式id的生成,使用毫秒时间戳、机器id、毫秒内的流水号来生成随机id。采用此种方法生成的id可以保证单机递增,但不能保证是全局递增的。

5.Netlify

很多人喜欢将自己的博客系统利用Github的Pages功能托管到Github上,但Github Pages并不支持对静态页面的构建,只能将构建完成后的页面推动到Github上。Netlify支持对静态网站的持续集成和持续部署,代码可以托管于Github上,Netlify会自动构建和发布,支持免费的https协议和CDN。

但可惜的是,实际测试下来,网站的访问速度在国内不是很理想。

6.996.ICU

全世界最大的同性交友社区Github上异常火爆的声讨996工作制度的项目,两天的时间内已经突破6万多Star,issue的数量也已经破万(截止到2019-03-28 19:55),要知道Github上Star数最多的项目freeCodeCamp也才接近30万Star,全世界运行设备最多的操作系统linux也才7万多Star,这简直创造了Github上Star数增长的奇迹。

很多人在issue中提到了加群交友吐槽、倾诉加班不满,甚至还有过来找男盆友的,活脱脱把issue玩成了贴吧,留言中清一色的汉字,说明基本是中国人在玩。

我个人对于强制996加班的事情不是很赞成,虽然过去三年中我的工作强度应该大于996,但更多的是出于个人自愿和对健康的无视,公司层面并没有强制要求。人生确实有非常多美好的事情可以参与和享受,对于程序员这个群体而言,电脑屏幕之外的世界还很大,还有太多的领域值得去探索和挖掘。但如果确实是因为个人的爱好,在工作中能够获得很好的成就感和满足感,996或者更高强度的加班,我个人觉得都是值得的。

说起ICU,程序员这个群的职业病是颈椎、腰椎、视力等,失眠多梦也是大有人在。我个人也确实身体出过一些问题而住过院,人往往都是在身体好的时候不懂得去重视自己的身体,当身体一旦出毛病的时候才懂得去珍惜。我曾经生病的时候也是鼓励自己要多锻炼和注意身体,但当身体好了之后,当时的愿望又抛到了脑后。

身体出现问题往往不是一朝一夕造成的,而是长期积累的结果,尤其是刚工作的前几年,趁着年轻确实能多加班熬夜,但30岁之后往往体力就跟不上了。还是奉劝各位,在工作的时候多注意休息和加强锻炼,无论是996,还是朝九晚五,都要多注意。

精彩文章

1.互联网运维工作

滴滴运维总监来伟在2017年对运维工作范围的思考,公司处在不同的阶段,运维所能干的事情也有所不同。

2.早点懂这几个道理,就不害怕被裁员了

IT行业中的一些职场规则,程序员在中年时期如果不做好职场转型,会逐步被更有活力更有体力的年轻人给取代。

golang中的panic用于异常处理,个人感觉没有try catch finally方式直观和易用。

func panic(v interface{})函数的作用为抛出一个错误信息,同时函数的执行流程会结束,但panic之前的defer语句会执行,之后该goroutine会立即停止执行,进而当前进程会退出执行。

func recover() interface{}定义在panic之前的defer语句中,用于将panic()进行捕获,这样触发panic时,当前gotoutine不会被退出。

recover所返回的内容为panic的函数参数,如果没有捕获到panic,则返回nil。

注意:recover仅能定义在defer中使用,在普通语句中无法捕获recover异常。recover可以不跟panic定义在同一个函数中使用。

example 1

1
2
3
4
5
6
7
8
9
import "fmt"

func main() {
defer fmt.Println(1)
fmt.Println(2)
panic(3)
fmt.Println(4)
defer fmt.Println(5)
}

panic()执行后,会先调用defer函数,然后打印panic: 3,当前goroutine退出,后续语句不再执行,程序输出:

1
2
3
4
5
6
7
8
2
1
panic: 3

goroutine 1 [running]:
main.main()
/Users/lvkai/src_test/go/panic/panic.go:8 +0xd5
exit status 2

example 2

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

import "fmt"

func main() {
func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recover: ", r)
}
}()
defer fmt.Println(1)
fmt.Println(2)
panic(3)
fmt.Println(4)
defer fmt.Println(5)
}()
fmt.Println(6)
}

在执行panic后,触发当前函数中的defer中的recover函数,此时panic后的当前函数中的语句同样是不再执行,但当前goroutine不会退出。也就是说panic被recover后,会影响到当前函数中的后续语句的执行,但不影响当前goroutine的继续执行,输出内容如下:

1
2
3
4
2
1
recover: 3
6

example 3

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

import "fmt"

func main() {
func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recover: ", r)
}
}()
func() {
defer fmt.Println(1)
panic(2)
}()
fmt.Println(3)
defer fmt.Println(4)
}()
fmt.Println(5)
}

recover跟panic定义在不同的函数中,仍然可以发挥作用。

1
2
3
1
recover: 2
5

春回大地,题图为即将融化的河面以及还在冰面上行走的路人。

资源

1.fastThread

在线的JVM线程栈分析工具,通过上传JVM Dump文件,在线查看线程分析结果。

2.全球空气质量地图

可以在线查看全球的PM2.5情况,很多国家的PM2.5都超过了200,但并不包含中国的数据,不知道是不是怕数据把其他国家吓死的缘故。

3.Walle

http://www.walle-web.io/docs/2/zh-cn/static/deploy-console.png

使用Python3开发的CI/CD平台,有相对友好的界面,目前Github Star数在6000+。

4.Træfɪk

为微服务而生的HTTP协议反向代理,自带API接口、dashboard,并支持Kubernetes Ingress、Mesos等,可动态加载配置文件等诸多nginx不具备的特性。

5.kcptun

https://github.com/xtaci/kcptun/blob/master/kcptun.png

基于KCP协议的UDP隧道,KCP协议能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%。

6.k3s - 5 less than k8s

有人搞了个k3s项目,作为轻量级的k8s,整个二进制包只有40M大小。项目定位为边缘计算、IoT、CI等,支持多种硬件平台,裁剪了k8s的很多功能,比如云依赖,存储插件等,甚至连k8s依赖的etcd存储都默认更换为了sqlite3。

7.Drone

基于Golang的Container Native的CD平台,Github上star 17000+,插件的安装也是基于容器的。

8.kaniko

通过Dockerfile来构建docker镜像,需要dockerd进程的支持,这在物理机上操作没有任何问题。而如果要想在容器中通过Dockerfile构建docker镜像却变得困难起来,因为dockerd的运行需要root权限,而容器为了安全是不建议开启root权限的。

该工具可以在容器中不运行dockerd的情况下通过Dockerfile构建出docker镜像。

9.Nacos

阿里巴巴开源的微服务框架,支持配置中心、动态服务发现、动态DNS。

10.PowerDNS

https://user-images.githubusercontent.com/6447444/44068603-0d2d81f6-9fa5-11e8-83af-14e2ad79e370.png

Linux下除了bind外的另一个可选择的DNS服务器,数据存储在mysql中,还有一个可选择的漂亮UI。

精彩书籍

要想回顾一下过去的十年中都发生了哪些大事,中国发生了哪些变化,经济领域里有哪些大起大落,本书可以拿来一读。

最近突发奇想,想折腾一下路由器。经过研究半天后,锁定了网件R6400这款路由器,原因是可选择的系统较多,跟华硕的路由器架构一致,且支持较为强大的梅林系统。

整个刷机的过程还是非常简单的,虽然花了我不少时间,本文简单记录一下。

拿到手后,R6400比我印象中的要大不少,可以通过下图中的苹果手机进行对比。

R6400有v1和v2两个版本,其中v1版本的CPU频率为800MHz,v2版本的CPU频率为1GHz。v1版本的刷梅林系统和v2版本刷系统有所区别,v2版本需要先刷DD-WRT固件作为过渡固件,然后再刷梅林固件。

整个的过程最好用有线连接路由器操作,用无线会频繁掉线。

.chk结尾的文件为过渡固件,.trx为最终固件。

注意:下载的固件文件最好检验一下md5,确保固件的正确性。

刷dd-wrt固件

连接上路由器后,在chrome浏览器中输入http://www.routerlogin.net可跳转到网件管理系统,通过一堆路由器设置后会重启路由器,然后重新登录。

选择“是”后会下载新网件固件,其实该步骤可以选择“否”即可。固件下载完成后,会升级固件,路由器会自动重启。

下载DD-WRT固件文件“DD固件.chk”,并在路由器的管理界面“高级 -> 管理 -> 路由器升级”中上传固件。

这里选择是,然后开始升级固件,升级完成后路由器会重启。

重启完成后,Wifi信号变成dd-wrt,没有密码,可直接连接。

在浏览器中输入192.168.1.1,会出现dd-wrt的界面。用户名和密码可以直接输入admin,因为该系统仅为中间过度系统。在dd-wrt这个偏工程师化的系统中有非常详细的信息,包括路由器的CPU和Memory等的硬件信息,甚至还有load average,多么熟悉的指标。

升级梅林固件

在dd-wrt的固件升级中选择“R6400_380.70_0-X7.9.1-koolshare.trx”,刷入梅林固件。待路由器重启完成后,即完成梅林固件的刷入。此时路由器的Wifi SSID变为“NETGEAR”。

访问192.168.1.1,会出现梅林系统的管理界面,依次设置即可。

题外话:无线的密码修改完后,悲剧的事情发生了,路由器重启后居然连不上wifi,提示密码错误。不得不找来一台带有网口的笔记本用有线连接。在梅林管理系统中查看,未发现密码输入错误,明明输入的密码是对的,但SSID换一个密码居然奇迹般的可以无线连接了,怀疑是一个bug。

设置完成后路由器会重启,此时管理系统地址变更为192.168.50.1。

在“高级设置 -> 无线网络 -> 专业设置”中,调整区域为“United States”,据说可以加快速度。

要想使用软件中心,需要在系统设置中开启下图选项,并重启路由器。重启后,Format JFFS partition at next boot会自动设置为false。

ASUS Router

下载app “ASUS Router”,可以直接连接到路由器,这是因为网件的路由器架构跟华硕完全一致。

ref

0%