在 Go 中获取 stacktrace

打印 stacktrace
1
2
3
4
5
6
buf := make([]byte, 1<<16)
// 获取 **所有** goroutine 的 stacktrace
runtime.Stack(buf, true)
// 如果需要获取 **当前** goroutine 的 stacktrace, 第二个参数需要为 `false`
runtime.Stack(buf, true)
fmt.Println(string(buf))

太诡异了,居然要指定 buffer 的大小,用起来不方便。虽然可以给个“足够大” buffer 用来容纳 stacktrace,但是什么是“足够大”?

为了确认 runtime.Stack() 函数的 behavior,需要参考一下 Go 输出 stacktrace 的实现代码。该代码是使用 GOC 写成的。

A .goc file is a combination of a limited form of Go with C.

mprof.goclink
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
func Stack(b Slice, all bool) (n int) {
uintptr pc, sp;

// Stack pointer
sp = runtime·getcallersp(&b);
// Programer pointer
pc = (uintptr)runtime·getcallerpc(&b);

// 如果选择输出所有 goroutine 的 traceback, 挂起所有goroutine,
// 在本函数完成后恢复
if(all) {
runtime·semacquire(&runtime·worldsema, false);
m->gcing = 1;
runtime·stoptheworld();
}

if(b.len == 0)
n = 0;
else{
// 重定向输出缓冲, 打印到 `b` 这个buffer里
g->writebuf = (byte*)b.array;
// buffer具有固定大小, 因此会截断
g->writenbuf = b.len;
// proc.c: runtime·goroutineheader
runtime·goroutineheader(g);
// traceback_${arch}.c
runtime·traceback(pc, sp, 0, g);
if(all)
runtime·tracebackothers(g);
n = b.len - g->writenbuf;
g->writebuf = nil;
g->writenbuf = 0;
}

if(all) {
m->gcing = 0;
runtime·semrelease(&runtime·worldsema);
runtime·starttheworld();
}
}

因为打印到buffer会截断过长的结果,因此可以写一个包装函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func StackTrace(all bool) string {
// Reserve 10K buffer at first
buf := make([]byte, 10240)

for {
size := runtime.Stack(buf, all)
// The size of the buffer may be not enough to hold the stacktrace,
// so double the buffer size
if size == len(buf) {
buf = make([]byte, len(buf)<<1)
continue
}
break
}

return string(buf)
}

例子可以在这里看到:Go Playground

使用阿里云镜像服务器

最近本机访问 163.comCentOS 镜像比较不稳定,体现在 ping 很低,但是 HTTP 连接很慢,yumfastestmirror 也不太理想,所以一般都禁用之。

发现阿里云的镜像服务器,立马换了,便秘立刻就通了。

yum 镜像

官方源镜像

1
2
3
4
5
6
# 禁用 fastestmirror 插件
sed -i.backup 's/^enabled=1/enabled=0/' /etc/yum/pluginconf.d/fastestmirror.conf
# 备份
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
# 使用阿里云镜像
wget -O /etc/yum.repos.d/CentOS-Base-aliyun.repo http://mirrors.aliyun.com/repo/Centos-6.repo

EPEL 镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装 EPEL 源
wget http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
wget http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
rpm -Uvh remi-release-6*.rpm epel-release-6*.rpm
# 使用阿里云镜像
if [[ ! -f /etc/yum.repos.d/epel.repo.backup ]]; then
mv /etc/yum.repos.d/epel.repo /etc/yum.repos.d/epel.repo.backup 2>/dev/null || :
fi

if [[ ! -f /etc/yum.repos.d/epel-testing.repo.backup ]]; then
mv /etc/yum.repos.d/epel-testing.repo /etc/yum.repos.d/epel-testing.repo.backup 2>/dev/null || :
fi

wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-6.repo

PyPi 镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mkdir -p ~/.pip
touch ~/.pip/pip.conf

sed -i.backup -r \
's/^index-url\s*=\s*.+$/index-url = http:\/\/mirrors.aliyun.com\/pypi\/simple\//' \
~/.pip/pip.conf

# If file not changed, write contents back to pip.conf
diff "~/.pip/pip.conf" "~/.pip/pip.conf.backup" &> /dev/null

if [ $? -eq 0 ]; then
cat > ~/.pip/pip.conf <<EOF
[global]
index-url = http://mirrors.aliyun.com/pypi/simple/
EOF
fi

RubyGems 镜像

1
2
gem source -r https://rubygems.org/
gem source -a http://mirrors.aliyun.com/rubygems/

使用 FPM 创建 Python 的 RPM 包

生成 RPM 包太麻烦了,最近知道了一个名为 FPM 的神器,在此记录一下。

安装 FPM

NOTE: 测试系统为 RedHat 系的 CentOS 6.3,编译 Python 2.7.6 的 RPM 包。

安装 Ruby

由于 FPM 使用 Ruby 写成,因此系统中需要安装 Ruby 的运行环境(这里 gem 的源改为了 taobao 的镜像):

1
2
3
4
5
6
# Install ruby dependencies
yum -y install ruby rubygems ruby-devel
# Use taobao repo for ruby gems
gem sources -a http://ruby.taobao.org/
# Remove origin repo from ruby gems
gem sources --remove http://rubygems.org/

通过 Gem 安装 FPM

Ruby 安装完成后,就可以使用 gem 安装 FPM 了:

1
2
# Install fpm
gem install fpm

Good.

设置编译环境

在编译 Python 之前,需要安装开发工具和库:

1
2
3
4
5
6
7
8
# Install EPEL repository
wget http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
wget http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
rpm -Uvh remi-release-6*.rpm epel-release-6*.rpm

# Install build toolchain
yum -y groupinstall "Development tools"
yum -y install openssl-devel readline-devel bzip2-devel sqlite-devel zlib-devel ncurses-devel db4-devel expat-devel

编译并创建 Python 的 RPM 包

FPM 的使用比较简单,可以参考 FPM使用说明

首先,下载 Python-2.7.6 的源码包并解压:

1
2
curl --progress-bar -LO http://mirrors.sohu.com/python/2.7.6/Python-2.7.6.tgz
tar xf Python-2.7.6.tgz

第二步,编译 Python-2.7.6

1
2
3
4
5
6
7
8
9
10
11
12
cd Python-2.7.6.tgz

# Python2.7编译安装后会安装到这个目录,方便打包
export INTERMEDIATE_INSTALL_DIR=/tmp/installdir-Python-2.7.6
# RPM包安装后Python2.7的目录
export INSTALL_DIR=/usr/local

LDFLAGS="-Wl,-rpath=${INSTALL_DIR}/lib ${LDFLAGS}" \
./configure --prefix=${INSTALL_DIR} --enable-unicode=ucs4 \
--enable-shared --enable-ipv6
make
make install DESTDIR=${INTERMEDIATE_INSTALL_DIR}

第三步,使用 FPM 创建 RPM 包:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 注意之前导出 INTERMEDIATE_INSTALL_DIR 和 INSTALL_DIR 这两个环境变量,这里还要使用
fpm -s dir -t -f rpm -n python27 -v '2.7.6' \
-d 'openssl' \
-d 'bzip2' \
-d 'zlib' \
-d 'expat' \
-d 'db4' \
-d 'sqlite' \
-d 'ncurses' \
-d 'readline' \
--directories=${INSTALL_DIR}/lib/python2.7/ \
--directories=${INSTALL_DIR}/include/python2.7/ \
-C ${INTERMEDIATE_INSTALL_DIR} .

Bonus Time

包含以下内容:

  • 自动下载、编译、打包 Python RPM 包的 Makefile;
  • 自动下载、编译、打包 virtualenv、pip、supervisor 等 Python 库和工具的 RPM 包。

GitHub 项目地址:python27-rpm

RabbitMQ笔记(三): Pika客户端(Python)发送大尺寸消息的问题

问题描述

这个问题存在很久了,现象就是使用 Pika 库的客户端在发送大尺寸消息后,RabbitMQ 没有收到,Consumer 那里会认为消息已丢失。

NOTE: 即使在本文写时的最新版(v0.9.13)依然存在这个问题。

分析

因为网络状态很好,所以没有考虑网络的错误,直接考虑Pika库的问题。

那么第一步就是把 Pika 的代码拿来读一遍,主要看它是怎么发送消息的,经过一番探索,找到了 pika/adapters/base_connection.py 这个文件,来看看里面的内容:

pika/adapters/base_connection.pylink
1
2
3
4
5
6
7
8
9
10
11
12
def _handle_write(self):
"""Handle any outbound buffer writes that need to take place."""
total_written = 0
if self.outbound_buffer:
try:
bytes_written = self.socket.send(self.outbound_buffer.popleft())
except socket.timeout:
raise
except socket.error, error:
return self._handle_error(error)
total_written += bytes_written
return total_written

看出问题了吗?

来看看socket.send的文档吧:

Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Returns the number of bytes sent. Applications are responsible for checking that all data has been sent; if only some of the data was transmitted, the application needs to attempt delivery of the remaining data. For further information on this concept, consult the Socket Programming HOWTO.

问题就是,没有检查 socket.send 的返回值,由于 socket.send 返回实际发送的直接个数,可能会小于期望(在这里是 self.outbound_buffer.popleft())。

由于 socket.send 可能没有将数据发送完成就返回了,造成了消息丢失的情况。

本来想自己修的,看了一眼 Pikamaster 分支,已经修正了(一次保证将一个帧(Frame)发送完成):

pika/adapters/base_connection.pylink
1
2
3
4
5
6
7
8
9
10
11
12
13
def _handle_write(self):
"""Handle any outbound buffer writes that need to take place."""
bytes_written = 0
if self.outbound_buffer:
frame = self.outbound_buffer.popleft()
try:
self.socket.sendall(frame)
bytes_written = len(frame)
except socket.timeout:
raise
except socket.error as error:
return self._handle_error(error)
return bytes_written

呵呵……

解决

  1. 自行打补丁;
  2. 使用 git 上的 Pika: pip install git+https://github.com/pika/pika.git
  3. 不用 Pika, 换其它的,比如 Kombu

我现在不用 Pika 了,用 Kombu

PS: Pika 的发布也太不积极了,都怎么久了还不发新版本。

解决GitHub Pages的302转向问题

起因

NOTE: 如果给GitHub Pages使用的是子域名,按照GitHub Pages文档配置,不会出现该问题。

由于我在GitHub Pages上的搭的博客使用了theo.im这个根域名(Apex Domain),在按照 GitHub Pages文档上提供的信息,对theo.im设置了A记录,但是通过执行命令:

1
curl -I http://theo.im/sitemap.xml

发现得到的是302转向,不符合Sitemap协议的要求:

A successful request will return an HTTP 200 response code; if you receive a different response, you should resubmit your request. The HTTP 200 response code only indicates that the search engine has received your Sitemap, not that the Sitemap itself or the URLs contained in it were valid.

解决

通过搜索,我发现需要使用ALIAS记录解析根域名,但是由于我之前使用的是DNSPod 的服务,不支持ALIAS记录,因此决定换加DNS域名解析商。

经过一番搜寻,找到了两家支持ALIAS记录的DNS域名解析商:

  1. DNSimple: 全收费服务
  2. PointDNS: 有免费的开发者账户

本着能用收费不用免费的原则,因此我选择了 DNSimple 来解析我的域名 (゜o゜(☆○=(-_-)

OK,万文不如一图:

最后,运行 dig theo.im +nostats +nocomments +nocmd 检查DNS是否生效:

1
2
3
4
; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.23.rc1.el6_5.1 <<>> theo.im +nostats +nocomments +nocmd
;; global options: +cmd
;theo.im. IN A
theo.im. 3600 IN A 199.27.79.133