知而智

  • 首页

  • 归档

  • 分类

  • 关于

golang开发中遇到的问题

发表于 2017-09-11 | 分类于 golang

问题总结

在用golang编写服务器程序的时候,遇到下面一些问题,所以在这里做一些记录,尤其是slice隐藏的bug较隐性,若对slice工作原理不了解是较难发现的。

  • slice元素被莫名替换
  • index out of range问题
  • 空指针引用
  • map的并发读写导致进程挂掉
  • String方法导致的递归死循环
  • 通道多次close(close of closed channel)
  • 死锁
  • 下次gc内存回收条件
  • 定时任务的问题

slice元素被替换

slice在开发中运用较频繁的基础数据类型,和数组相似,与数组最大的区别在于他是系统自动扩容的。

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}

上面是go runtime里面对slice实现数据结构,指向一块内存的一个指针、一个当前slice里面的长度以及当前slice里面的最大容量。可以预想下
若另外一个指针同之前slice的指针有内存重叠的话,修改重叠部分是会相互影响的,下面我们用实例来确定下这个问题。

test1.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sa := []byte{}
sa = append(sa, 1, 2, 3, 4, 6)
sb := sa
sb = append(sb, 5) // sb = append(sa, 5) 同样有问题
// 从小到大排序
algorithm.Sort(sb, 0, len(sb)-1)
fmt.Printf("len: %d cap: %d of slice sa: %v\n", len(sa), cap(sa), sa)
fmt.Printf("len: %d cap: %d of slice sb: %v\n", len(sb), cap(sb), sb)
// output:
// len: 5 cap: 8 of slice sa: [1 2 3 4 5]
// len: 6 cap: 8 of slice sb: [1 2 3 4 5 6]

test2.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sa := []byte{}
sa = append(sa, 1, 2, 3, 4, 5, 7, 8, 10)
sb := sa
sb = append(sb, 9)
// 从小到大排序
algorithm.Sort(sb, 0, len(sb)-1)
fmt.Printf("len: %d cap: %d of slice sa: %v\n", len(sa), cap(sa), sa)
fmt.Printf("len: %d cap: %d of slice sb: %v\n", len(sb), cap(sb), sb)
// output:
// len: 8 cap: 8 of slice sa: [1 2 3 4 5 7 8 10]
// len: 9 cap: 16 of slice sb: [1 2 3 4 5 7 8 9 10]

上面两个程序的输出可能看出,test1.go中对sb排完序后sa和sb第5个元素是被替换掉了的。
实际上sa和sb是两个指向同块内存的不同指针,只是它们的len值不一样,需要注意的是sb调用
append后cap的值还是和sa的一样。和test2.go形成鲜明的对比,只要slice的长度超过了默
认的容量后就会自动扩容。

sb:=sa之后的代码写在函数里面同样有这样的问题,因为go里面传参都是按值传递的

index out of range

  • 索引slice元素越界
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sa := []byte{}
sa = append(sa, 1, 2, 3, 4, 6)
// sa[5] = 20 // 这里会panic
sb := sa[5:]
// sb[0] = 5 // 这里会panic
fmt.Printf("len: %d cap: %d of slice sa: %v\n", len(sa), cap(sa), sa)
fmt.Printf("len: %d cap: %d of slice sb: %v\n", len(sb), cap(sb), sb)
// output:
// len: 5 cap: 8 of slice sa: [1 2 3 4 6]
// len: 0 cap: 3 of slice sb: []

对于这种下标越界的问题是当索引的下标号超出了len(sa)的时候就抛出越界的错误

  • slice表达式越界问题

根据官方文档的说明slice表达式有两个,一种简单的表达式和参数更全的表达式
分别对应a[low : high]和a[low : high : max]

spec说明
对于这类越界是下标不在0 <= low <= high <= max <= cap(a)这范围时会出现问题

空指针引用

相信不论是C系还是JAVA开发者都会遇到空指针的问题,C里面对空地址的访问是致命的,
并且不像JAVA能把这种异常捕获,go也不例外,为了保证服务正常运行同样可以通过
recover把所有runtime时的错捕获到,但并发读写的错误是不行的。所以我们项目里基本
在所有的协程里面有recover输出的。

项目中我们把一个自定义的interface的变量赋值一个nil,但去执行原先持有的类型的
函数时就panic掉了,所以对默认值为空值引用的都会出这种错误,比如:空指针和接口

有趣的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Stu struct {
Name string
}
func (s *Stu) GetName() int {
fmt.Printf("s is %v", s) // 这里能正常调用,但不要再去调Stu里面的成员
return 2
}
func main() {
var s *Stu
s.GetName()
}

上面的例子一开始出乎意料,但仔细一想其实是符合go的设计理念的,方法是属于类型的,
类型的变量当然是可以正常调用的,但变量为空再去调用里面的成员就是对空指针的访问了。

map的并发读写导致进程挂掉

对一个多线程程序来说对数据竞争的保护是必不可少的,但在处理日志输出的时候,如果
一个对象有map或数组之类的并对有并发读写这很容易导致进程panic掉。所以打印日志的
时候一定要注意不要输出一个对象。

String方法导致的递归死循环

在日志输出的时候打印了一个对象并且对象实现了String()方法,如果String方法里面
又调了自身,这种使用方法是很危险的,在我们开发期间是有同事犯了这个一个错误的。

死锁

既然是并发程序,难免会有数据竞争的问题,所以我们用了互斥锁来解决这个问题,
如果获取锁和释放锁没有成对使用或者是锁重入(递归获取锁)就会导致死锁的风险,
因为我们项目是针对房间的,虽然不影响所有的人,一出现死锁只会影响这个房间
的人,不过我们这种锁是睡眠锁,并不消耗CPU。

我们的产品是一款地方麻将,用golang开发的

死锁1:

我们的产品上线第二天就出现导致全服(多个逻辑服)服务不可用。后来经过排查是有死锁影响了全局。

一开始并不知道问题在哪,有以下两个解决方案:

  • 新增高版本的逻辑服(不影响新的玩家)
  • 停服维护

一开始我们选择的是一方案,但好像出了点问题,又换了二方案,二方案虽然可以但是已经开的房间的钻石没返还。
弄了好久才弄程序把需要返还钻石返还。

不过好在上线前有人说要开放pprof接口,经过日志分析是有很多goroutine阻塞在获取锁的地方。
上线前我们做了一个需求,10分钟没开局的房间需要解散掉。也没考虑太多,觉得这个需求比较
简单,所以逻辑服这边起了个定时任务1分钟扫下单个逻辑服的房间列表。

定时任务我们是用的读锁来执行的,从列表里面删除一个房间的是写锁的。读锁是可以多次获取的。
假如1分钟还没遍历完所有的房间,另外一个定时任务立即执行,又重新获取房间列表的读锁。1分
钟遍历的时候是有获取房间的锁的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
R*是获取房间里面的锁
RA ——> roomA
RB ——> roomB
再尝试获取房间列表写锁的:
RA ——> list
RB ——> list
T*是定时任务得到房间列表的读锁:
T1 ——> list
T2 ——> list
T3 ——> list
再获取房间里面的锁
TA ——> roomA
TB ——> roomB

定时任务是有获取房间的锁的,如果刚好TA的锁等着RA的锁就会产生死锁

后续我们的解决方案是为每一个房间启一个定时。能不用锁解决的问题千万不要不用,获取锁是会导致上下文的切换,
从一执行线程切换到另一个执行线程。

golang的channel就是通过锁来实现不同的协程之间可以通信。虽然用到了锁,但是这个锁对开发人员是无感的,所
以对使用者来说就安全了。

死锁2:

这个问题是异常情况下面导致的单个房间的死锁,当时写代码的时候能不用defer就不用,所以在一个加锁的函数的
地方没有用defer解锁,在函数执行当中有panic,所以没执行后面解锁的函数。如果用了defer解锁就不会有这个问
题了。所以在有锁的情况尽量用defer就用defer。

下次gc内存回收条件

线上因为通过pprof看到heap一直在增长,一天增长了一千多。用top观察实际res也增了几十兆的物理内存。
因此网上找了很多关于linux内存管理以及golang的pprof里面的heap各字段详细描述。

(MemStats)[https://golang.org/pkg/runtime/#MemStats]

通过线上实际表现,内存是一直往上增的情况,所以看了下哪里可能有内存没被回收掉,以及GC的回收机制。

一开始自己有以下疑问:

  • 有垃圾回收的语言不是会自动回收的吗?
  • 有回收的话哪些对象没被回收掉?

一开始对代码大概的看了一下,除了其它一些频繁make的切片(也就十几个byte)的长度,这些部分是由他底层内存分配
管理器来控制的,我是束手无从的,不过也对一些频繁用到的对像用了内置的pool对象池。所以从其它地方想下还有哪
些地方没被回收掉,后来想到定时器!定时器确实也是有泄漏的(有些没有Stop掉)。

1
2
export GODEBUG=GOGC=50,GCTRACE=1
./main

默认GOGC是100的,所以gc会在下次分配的内存和上次分配的内存的比是成倍的才会把内存回收掉,设置成50后
通过压测试后通过日志输出会发现回收的次数相对来说要频繁,所以内存没有出现大的增长,停下来的时候恢复
平稳的值,但设置成50的时候会偶发的卡顿。线上估计也只能设置成80左右吧!暂时这个值没放到线上去跑。

参数说明

定时任务的问题

参考:
实现说明

go里面使用定时器有以下几种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer{
C: c,
r: runtimeTimer{
when: when(d),
f: sendTime,
arg: c,
},
}
startTimer(&t.r)
return t
}
type runtimeTimer struct {
i int
when int64
period int64
f func(interface{}, uintptr) // NOTE: must not be closure
arg interface{}
seq uintptr
}
func AfterFunc(d Duration, f func()) *Timer {
t := &Timer{
r: runtimeTimer{
when: when(d),
f: goFunc,
arg: f,
},
}
startTimer(&t.r)
return t
}
func goFunc(arg interface{}, seq uintptr) {
go arg.(func())()
}
time.NewTicker(time.Second)
time.After(time.Second)
time.AfterFun(time.Second, func(){})

通过源码以及使用接口总结,主要有两种定时器。1) 执行一次的 2) 定期执行的。通过数据结构
我们看了下不同接口实际的区别只在period字段。以及这个f参数用的默认的sendTime是非
阻塞的调用,AfterFun和前面两个接口不同的时没通过管道来实现定时器到点了(可以执行我的
业务了),而它是直接用的goFunc包装在自己的协程里面执行

1
2
3
4
5
6
7
8
9
10
for i := 0; i <= 1000000; i++ {
go func() {
for {
select {
case <-time.Tick(time.Second):
return
}
}
}()
}

如果像上面的代码用time.Tick()创建的定时器(也就是管道)没Stop()掉,定时器任务
列表会一直规程,导致内存一直无法得到释放。尤其是循环条件加大到百万级别的时候就
更明显了。

enjoy hexo

发表于 2017-09-08

搭建博客的过程

对于没有购买自己域名的个人来说hexo+github是个不错的选择。

安装步骤:

  • 安装git
  • 安装nodejs
  • 在git上创建自己的仓库
  • 用hexo创建博客并生成静态页面,关联自己的仓库,再hexo deploy基本就好了

前期准备工作

  • git下载
  • nodejs下载 选择LTS(Long Term Support)版本就好
  • npm i -g hexo安装hexo

npm是nodejs来管理软件库的一个工具,i是安装-g 是全局安装,安装后尽情享受hexo –help的功能吧。
更换主题地址git clone url themes/

enjoy

所有准备工作做好后直接用hexo init name初始化一个博客目录

  • hexo new first-post来创建自己的需要写的文章
  • hexo server就可以通过访问localhost:4000博客地址了

可以在配置文件里面支持分类、评论和主题等各种参数配置

在配置git上面的仓库的时候用git协议每次更新就不需要输入密码~

发布到github

把自己本地的ssh的公钥放到github个人帐号的key下面,没有则用ssh-keygen一直回车生成,cat ~/.ssh/id_rsa.pub
在配置里面自己github仓库地址

仓库名格式:名称+github.io example:hello.github.io,否则无效

每次有新文章需要发布的时候执行下面命令:

1
2
3
hexo clean
hexo generate
hexo deploy

lock

发表于 2017-03-14 | 分类于 linux-c

os是多任务操作系统,同一个进程下面是可以有多个线程(进程)同时运行的,线程实际也是一个进程,只是对父进程的一个copy,与父进程共享地址空间进程,所以对同一进程共享的资源需要进行同步,同步的方法包括mutex semaphore spinlock 条件变量

很多东西如果只停留在概念层面远远是不够的,我们还是要不断的思考下面的实现机制和原理,下面说下同步的东西。

semaphore

1
2
3
4
5
6
7
8
9
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
};
typedef struct {
volatile int counter;
} atomic_t;
阅读全文 »

linux-io

发表于 2017-03-14 | 分类于 linux-c

少说话,多看代码,多思考,多动手

io操作是一个程序必不可少的部分,io可以是对块设备的读写,文件的读写,基于网络上的io,这里通过内核select和epoll源码比较下这两种io的优劣。平常服务器需要处理客户端多个连接(基于tcp)的io事件。

对io的读写是有阻塞和非阻塞之分的,对于tcp连接上的读写实际是基于它的读写缓存区,如果fd是阻塞的话,对读来说,缓冲区没数据,对写来说,缓冲区不足够装下需要写的数据或已经满了同样会阻塞。下面是一个阻塞和非阻塞读取标准输入的程序。

阅读全文 »

c和lua交互

发表于 2017-03-13 | 分类于 linux-c

要理解Lua和C++交互,首先要理解Lua堆栈。

简单来说,Lua和C/C++语言通信的主要方法是一个无处不在的虚拟栈。栈的特点是先进后出。

在Lua中,Lua堆栈就是一个struct,堆栈索引的方式可是是正数也可以是负数,区别是:正数索引1永远表示栈底,负数索引-1永远表示栈顶。如图:

阅读全文 »

最大可打开的文件数

发表于 2017-03-08 | 分类于 linux-c

平常开发项目的时候会遇到进程下打开的最大fd超过默认设置,导致用户不能正常使用服务。linux是基于文件系统的一个操作系统,操作系统最基本的单元就是进程。最大可打开的文件描述符分为系统级别的和进程级别的。

面向全系统的

虽然操作系统是基于文件系统的,但它对整个系统中所打开的最大fd数目也是有限制的,默认的系统最大的fd在/proc/sys/fs/file-max文件里,记住proc目录只是一个虚拟的,实际控制的值是在/etc/sysctl.conf里可通过以下方式修改:

1
2
vim /etc/sysctl.conf #fs.file-max=795165把这个放到文件最后
sysctl -w fs.file-max=795165

最后需要执行sysctl -p生效,再到cat /proc/sys/fs/file-max验证已经同步

<! – more –>

面向进程

我们知道linux系统是一个多用户多进程的多核操作系统,进程是属于具体某一个用户的,通过xshell等工具ulimit看到的东西是对这个用户的一些资源限制,默认的最大fd数是1024,这一般不能满足于我们的需求,所以需要我们手动修改
如果默认从终端(shell环境)启动的进程会继承这些参数,修改可以通过以下几种方式:

  • /etc/profile /etc/bashrc ~/.bash_profile ~/.bashrc 等脚本初始化地方加入ulimit -n value

  • vim /etc/security/limits.conf 按里面的说明还以为是对用户所有打开的fd有限制,经过测试并不是,对资源的限制还是针对进程的,只是用户登录shell会读取这个默认的值

ulimit -n value value值不能超过硬链接数 ulimit -Hn可以查看硬链数,最好在/etc/security/limits.conf设置自己想要的值

参考

docker.md

发表于 2017-03-07 | 分类于 容器

docker介绍

docker是用google开源语言golang开发的一款类似vm这样的管理容器的开源软件,他的优势在于不像vm一样启动一个虚拟机需要等待漫长的时间,而对docker而言只需要简单的启动一个image就相当于运行一个独立的container。各个container是相互独立的。启停一个container也是相对方便和快速的

此笔记记录阅读docker官网及博客并实践操作所记

  • docker和vm的区别
  • docker实践讲解
阅读全文 »

kubernete

发表于 2017-03-07 | 分类于 容器

这个世界最伟大的就是思想,人这一特种对这世界的改造都取决于他们独有的思想,他们用思想来创造或改造这个世界。而思想的优越之处在于把事物抽象化,技术的实现就是对现实世界的抽象。人真是太聪明了

kubernete

容器化技术在最近一两年可谓是风生水起,自从docker出世以来就爱到程序界的追棒,而去年google家亲生的kubernete也是更受亲耐,下面通过官网的手来学习记录。

Production-Grade Container Orchestration

Automated container deployment, scaling, and management

这是官网的标语.生产级别的容器编排,自动化进行容器部署、平衡、管理

通俗的说kubernete就是一个容器管理系统,我们的app就运行在这些容器上,官网上说google在kubernete一周运行了上亿的容器。管理这么多的容器,要是传统的模式这么多台服务器该怎么管理呀,真是太解放了。

kubernete集群

kubernete集群是把多台机器抽象成一个独立的单元,并保证集群的高可用性。这样一来我们发布我们的应用的时候不需要指定到某一机器,只要把我们的应用容器化就可以部署在集群中,并不需要关心部署在某台机器上。
kubernete会以高效的方式在集群中发布和调度这些容器

一个集群概念包含两种类型的资源:

  • Master 用来协调各个Node节点
  • Node 运行我们的应用
阅读全文 »

NSQ

发表于 2017-03-03 | 分类于 开源代码学习

NSQ说明

NSQ是一个分布式的实时的消息队列,基于GO语言开发,它的分布式是无中心节点的结构,据说是高可用,上亿的消息毫无压力,并且部署非常简单,之前项目里也有用,没详细读过代码,下面来说下NSQ的结构和实现

NSQ的组成部分

主要由以下三个daemon进程组成。

  • lookupd

维护nsqd节点以及他的topic和channel,方便client可以获取nsqd,也就是nsq里面的Producer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type RegistrationDB struct {
sync.RWMutex
registrationMap map[Registration]Producers
}
type Registration struct {
Category string
Key string
SubKey string
}
type Registrations []Registration
type PeerInfo struct {
lastUpdate int64
id string
RemoteAddress string `json:"remote_address"`
Hostname string `json:"hostname"`
BroadcastAddress string `json:"broadcast_address"`
TCPPort int `json:"tcp_port"`
HTTPPort int `json:"http_port"`
Version string `json:"version"`
}
type Producer struct {
peerInfo *PeerInfo
tombstoned bool
tombstonedAt time.Time
}
type Producers []*Producer
阅读全文 »

skynet

发表于 2017-03-02 | 分类于 开源代码学习

skynet源码阅读及实践总结

skynet是一个云风大大开源的一款基于c+lua的框架,架子由c语言编写完成,逻辑上用lua来实现的一个框架

如果不关心框架的话,至少需要知道这框架里面有些什么东西,知道他的来龙去脉,由于工作原由断断续续的深入了解这个框架。由于日常开发没太在意底层实现原理,有时看下底层代码后当时知道了原理,之后再来看时基本忘记得差不多了。所以由此需要做一些笔记和深入的思考


1. 两个服务之间通信就必须知道消息发送方和消息接收方以及消息和消息的长度,skynet里面对发送一条消息的参数注释如下:

1
2
3
4
5
6
7
uint32_t address
string address
uint32_t type
uint32_t session
string message
lightuserdata message_ptr
uint32_t len
  • address 指目标接收方的服务,服务用一个uint32的值来标识,也可以为一个服务取一个string类型的名称来唯一
  • type 指发送的消息的类型
  • session 指消息发送方对此次发送这条消息的会话标识,默认传0,传空底层会为其生成一个
  • message 指消息内容,消息内容有可能也是一个message_ptr
1
2
#define PTYPE_TAG_DONTCOPY 0x10000 //用 type | PTYPE_TAG_DONTCOPY 来指定消息是否需要copy一份
#define PTYPE_TAG_ALLOCSESSION 0x20000 //用 type | PTYPE_TAG_ALLOCSESSION 来指定是否需要创建session
阅读全文 »
12

易斌

20 日志
5 分类
19 标签
© 2018 易斌
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.2