Aug 10

记 python 超时的一个坑 不指定

felix021 @ 2019-8-10 13:34 [IT » 其他] 评论(0) , 引用(0) , 阅读(61) | Via 本站原创
# 背景

有一个 python 脚本调用 A 服务的 x 接口获取若干 GB 的数据(大量对象),读取和解析大约需要 5 分钟。

由于 x 接口的改造,需要改成调用 B 服务的 y 接口。

A、B 服务都是基于字节跳动的 KITE 框架开发的(今日头条Go建千亿级微服务的实践),通信协议是 thrift 0.9.2 。

# 现象

改成调用 B 服务,在测试过程中发现,每次大约到 3 分钟以后就会出现报错 TTransportException(TSocket read 0 bytes);之后会重试,但第一次报错后,之后每次大约1分钟内就会再次报同样的错误,重试 3 次后放弃、进程退出。

# 排查

1. 由于测试的时间是晚高峰,初步判断可能是晚高峰服务端压力太大导致。次日平峰期测试仍然复现。

2. 搜索,发现有人遇到类似问题,是从 thrift 0.9 升级到 1.0,服务端没有进行 utf8 编码导致客户端的解析问题,他通过修改服务端代码解决。然而服务端显然不存在问题,因为其他的服务调用该接口表现稳定。此外我遇到的问题并没有升级thrift版本。

3. 还是从报错信息入手,在代码里搜索 "TSocket read 0 bytes",来自于 python2.7/site-packages/thrift/transport/TSocket.py

  def read(self, sz):
    try:
      buff = self.handle.recv(sz)
    except socket.error, e:
      if (e.args[0] == errno.ECONNRESET and
          (sys.platform == 'darwin' or sys.platform.startswith('freebsd'))):
        self.close()
        buff = ''
      elif e.args[0] == errno.EINTR:
        buff = self.handle.recv(sz)
        if len(buff) > 0:
          return buff
      else:
        raise
    if len(buff) == 0:
      raise TTransportException(type=TTransportException.END_OF_FILE, message='TSocket read 0 bytes')
    return buff


4. 通过插入调试代码,发现并没有抛出异常,说明确实只读到了 0 字节,因此可以大致判断问题发生在 server 端。

5. 查看 B 服务的 log,发现确实有“客户端超时” 的报错信息。通过查看 KITE 框架的文档,发现默认的超时时间是 3 秒,A服务在配置文件里指定了 20s 的超时时间,而 B 服务没有指定。

6. 通过修改 B 服务的超时时间,调用成功。但为什么 python 作为一个客户端,会出现长达 3s 的停顿导致超时呢,尤其是在局域网甚至本机环境,不可能是网络原因。

7. 联想到曾经踩过的一个坑(详见:https://www.felix021.com/blog/read.php?2142),猜测是python的gc导致。虽然python是引用计数,但为了避免循环引用导致的内存泄漏,还是有一个 stw 的 gc 扫描。通过关闭这个扫描,就解决了这个超过 3s 的停顿

import gc
gc.disable()


# 吐槽

python真是慢。同样一个api,golang只要17s就完成了调用、反序列化,而python需要长达5分钟。
Aug 10
1. 现象

某日线上服务报警(基于时序数据库做的),请求量大幅下滑。

观察ganglia监控图表,发现有部分机器CPU占用率断崖式下跌,从原先的1000%+下降到100%(20核40线程的CPU),通过内存监控可以确认服务本身并未重启。

登录异常机器,用 lsof -i :PORT1 也可以看到端口号仍被占用,但是却无法接受请求;同样,该服务的 pprof 监听端口 PORT2 能接受请求,但无响应请求。

2. 排查

在异常机器上通过  "netstat -antpl | grep CLOSE_WAIT | grep PORT1 | wc -l" 可以看到有大量连接等待关闭,达到连接上限,所以无法接受请求,PORT2 则正常。

挑一台机器重启后服务恢复正常。为了恢复业务,重启了大部分异常机器上的服务,保留两台持续排查。

使用 top 查看cpu占用率,如上所述,始终保持在 100% 左右,说明有一个线程在全速运行。

通过 perf top (注:也可以使用pstack,能看到更详细的调用栈),发现是某一个二分查找的函数在占用cpu。

经过对代码的分析,发现是传入的值小于有效查找范围,代码实现不完善,导致出现了死循环。

进一步排查发现某个api未做好数据有效性保护,导致出现无效数据。

3. 分析

问题的直接原因已经定位,但是不明确的是,为什么一个 goroutine 死循环会导致进程整体hang住?

cpu 占用率只有100%,说明只有一个 goroutine 死循环,根据 Go 的 GMP 模型,理论上应该可以schedule其他的 goroutine 继续接受请求。

查看该进程的线程数(cat /proc/PID/status),看到开启了80+系统线程,说明不是线程数量的问题。

尝试查看该进程的 goroutine 数,但 pprof 不可用,而负责这个 metrics 打点的 goroutine 自从异常以后也未在上报数据。

写了一个简单的样例代码,开启一个简单死循环的goroutine,并不会阻碍其他goroutine的执行。
func test1() {
        fmt.Println("test1")
        i := 0
        for {
                i++
        }
}

func main() {
        go test1()
        for i := 0; i < 100; i++ {
                time.Sleep(1 * time.Second)
                fmt.Printf("i = %d\n", i)
        }
}


有一位同学根据现象搜到了这篇文章: 如何定位 golang 进程 hang 死的 bug

根据这篇文章的分析,在有死循环goroutine的情况下,其他goroutine主动调用 runtime.GC() 会出现 hang 住的情况。

验证代码如下,确实符合预期,和前述事故的表现一致。

func test1() {
        fmt.Println("test1")
        i := 0
        for {
                i++
        }
}

func main() {
        go test1()
        for i := 0; i < 100; i++ {
                time.Sleep(1 * time.Second)
                fmt.Printf("i = %d\n", i)
                if i == 3 {
                        runtime.GC()
                }
        }
}


综合以上分析可知,当 golang 中出现一个死循环的 goroutine、该 goroutine 就会一致占用 cpu,无法被调度;而需要 STW 的 gc 又无法暂停该 goroutine,因此出现了一个调度上的死锁。

另外,根据那篇文章的说法,在 for 循环中没有函数调用的话,编译器不会插入调度代码,所以无法完成抢占式调用。《深入解析Go - 抢占式调度》中也有具体的说明 https://tiancaiamao.gitbooks.io/go-internals/content/zh/05.5.html

实际测试发现,如果是调用自己写的另外一个简单函数,仍然会出现死锁,而调用注入 fmt.Println 之类的函数,则不会出现死锁,说明Go并不是在函数调用的时候插入调度检测代码(这也不符合直觉,每次函数调用都有额外性能开销,不太划算),而是在某些库函数中增加了调度检测。

完。

Apr 24
这是罗凯同学内部《Go 快速入门》课程第五讲的作业。

第一题:使用 channel 完成打印1000以内的素数

package main

import "fmt"

func prime(c chan<- int) {
    i := 2
    for {
        isPrime := true
        for j := 2; j < i; j += 1 {
            if i % j == 0 {
                isPrime = false
            }
        }
        if isPrime {
            c <- i
        }
        i += 1
    }
}

func main() {
    c := make(chan int)
    go prime(c)
    for {
        p := <-c
        if p >= 1000 {
            break
        }
        fmt.Println(p)
    }
}



第二题:等价二叉查找树,来自 Go tour:https://tour.go-zh.org/concurrency/7

原题使用 tree.New(1) 来生成一个包含10个元素的二叉查找树,简单的实现可以基于这一点,正好从channel里读出10个数字。

以下这个版本的实现更复杂一些,不假定二叉查找树的长度,所以加一个 wrapper,用来 close channel 。

package main

import (
    "fmt"
    "golang.org/x/tour/tree"
)

// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
func Walk(t *tree.Tree, ch chan int) {
    if t == nil {
        return
    }
    Walk(t.Left, ch)
    ch <- t.Value
    Walk(t.Right, ch)
}

func WalkWrapper(t *tree.Tree, ch chan int) {
    Walk(t, ch)
    close(ch)
}

// Same 检测树 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *tree.Tree) bool {
    c1 := make(chan int)
    c2 := make(chan int)
    go WalkWrapper(t1, c1)
    go WalkWrapper(t2, c2)
    for {
        v1, ok1 := <-c1
        v2, ok2 := <-c2

        if ok1 == false && ok2 == false {
            return true
        } else if ok1 == false || ok2 == false {
            return false
        } else {
            if v1 != v2 {
                return false
            }
        }
    }
}

func main() {
    t1 := &tree.Tree{&tree.Tree{nil, 1, nil}, 2, &tree.Tree{nil, 3, nil}}
    t2 := &tree.Tree{&tree.Tree{&tree.Tree{nil, 1, nil}, 2, nil}, 3, &tree.Tree{nil, 4, nil}}
    fmt.Println(t1)
    fmt.Println(t2)
    fmt.Println(Same(t1, t2))

    t3 := tree.New(1)
    t4 := tree.New(1)
    fmt.Println(t3)
    fmt.Println(t4)
    fmt.Println(Same(t3, t4))
}
Mar 6

TourDeBabel 不指定

felix021 @ 2019-3-6 11:54 [IT » 其他] 评论(0) , 引用(0) , 阅读(336) | Via 本站原创
转自:https://code.google.com/archive/p/windows-config/wikis/TourDeBabel.wiki
原文:https://sites.google.com/site/steveyegge2/tour-de-babel

(无意中翻出了很多年前看过的这篇文章,发现是在google code上的,那就转载作为存档吧)

通天塔导游

(译注:圣经记载:在远古的时候,人类都使用一种语言,全世界的人决定一起造一座通天的塔,就是巴别塔,后来被上帝知道了,上帝就让人们使用不同的语言,这个塔就没能造起来。 巴别塔不建自毁,与其说上帝的分化将人类的语言复杂化,不如说是人类自身心灵和谐不再的分崩离析。之所以后来有了翻译,不仅是为了加强人类之间的交流,更寄达了一种愿望,希望能以此消除人际的隔阂,获求来自心灵的和谐及慰藉。真正的译者,把握血脉,抚平创痕,通传天籁,开启心门。)

这是我写的旋风式的编程语言简介—我本来为亚马逊开发者杂志本月的期刊写的,但是发现我写的东西没法…见人。

首先,我偶尔一不小心口出脏话,或者对上帝不恭的话,所以对很官方很正式的亚马逊上发表是不合适的; 所以我就把它塞到我的博客里了,我的博客反正没人看的。除了你以外。是的,只有你会看,你好啊。

其次,这是一项进行中的工程,现在只是东打一耙西搞一下,还没有精加工过的。又一个把它写到博客里的很大的理由。不需要很好,或很完整。就是我今天想说的一些话。请随便!

我的旋风式简介会讲C,C++,Lisp,Java,Perl,(我们在亚马逊用到的所有语言),Ruby (我就是喜欢),和Python,把Python加进来是因为—好吧,你看了就知道了,现在我可不说。

C

你必须懂C。为哈? 因为出于所有现实的理由,这个世界上你过去,现在,将来会用到的每一台计算机都是一台冯·诺曼机器,而C是一种轻量级的,很有表达力的语法,能很好的展现冯·诺曼机器的能力。
Nov 23

浏览器客户端证书 不指定

felix021 @ 2018-11-23 23:06 [IT » 其他] 评论(0) , 引用(0) , 阅读(595) | Via 本站原创
2015年,从某传统金融国企跳槽来到我司的时候,发现后台管理系统竟然需要安装客户端证书才能登陆,简直惊为天人,通过利用 https 的客户端认证,配合证书中嵌入的用户名做权限控制,把内部系统的入侵难度至少增加了一个量级(当然,安装证书的过程对于非技术线的同学说也麻烦了不少)。

后来发现,原来是把 github.com/OpenVPN/easy-rsa 这个项目包装了一下实现的,其实也并不是很困难。

今年年初因为新项目也需要这个方案,自己心血来潮,参考网上的一些说明,用 openssl 的 genrsa、req、x509、pkcs12 这几个命令试着自己颁发客户端证书,并且包装了一套脚本,勉强能用。

但当时没有太多时间,吊销的功能并没有做,因为比颁发证书麻烦多了,不只是敲几个命令,还需要一套更复杂的方案,包括维护一个证书信息列表、按一定规范的文件目录结构,以及DIY的 openssh 配置文件等。

最近抽了两个晚上把整个流程重新梳理了一遍,填了几个坑,终于做了一套完整的脚本出来,这才好意思写这篇博客介绍一下。

这套脚本可以在这里获取:

  https://github.com/felix021/openssl-selfsign

使用起来可以说是非常简单了:

1. 创建CA

  $ ./1-sign-site.sh dev.com

会创建 ca 证书,并在 cert/site/dev.com/ 下面创建 *.dev.com 的 https 证书,并且生成一个 nginx.conf 配置文件供参考(直接可以用的)。

2. 颁发客户端证书

  $ ./2-sign-user.sh test1

在 cert/newcerts/test1-01/ 下面创建 test1 用户的一个客户端证书 cert.p12 ,并给出对应的密码,双击按提示导入即可。

3. 参考第一步生成的 nginx.conf 配置文件,配置好 web 服务器,就行了。

4. 稳妥起见,应当在代码中读取 http 头里的 SSL_DN 参数,从中获取邮箱或者用户名来作为系统的用户名。

至于吊销的过程,要更复杂一些,可以参考该项目的 README 。
Feb 28
留档备查

脚本里设置了 ServerActive ,会主动尝试到zabbix server注册,但需要先在zabbix frontend的 configuration->actions->auto registration 配置好 add host 动作,这样才会自动添加。

引用

#!/bin/bash

set -x

ZABBIX_SERVER=192.168.1.100

wget http://repo.zabbix.com/zabbix/3.2/ubuntu/pool/main/z/zabbix-release/zabbix-release_3.2-1+xenial_all.deb
dpkg -i zabbix-release_3.2-1+xenial_all.deb
apt update
apt -y install zabbix-agent

ip=`ifconfig  | grep -o 'inet addr:172\.[0-9.]*' | awk -F: '{print $2}'`
sed -i \
    -e 's/^Server=.*$/Server='$ZABBIX_SERVER'/' \
    -e 's/^ServerActive=.*$/ServerActive=lan.zabbix.thebitplus.com/' \
    -e 's/^Hostname=.*$/Hostname='`hostname`'/' \
    /etc/zabbix/zabbix_agentd.conf

sudo update-rc.d zabbix-agent enable
sudo service zabbix-agent restart

echo "Done"
Feb 11
莫名其妙的一个错误,手头的两个sentry实例里都没有这个表,但是还是会报这个错。

没研究具体的代码,但是通过查找源码里的 sentry_email 发掘了表结构,建表并授权即可:
引用
$ psql -h $HOST -U root -W sentry
sentry=> create extension citext;
CREATE EXTENSION
sentry=> create table sentry_email (id bigserial primary key, email CITEXT, date_added timestamp with time zone);
CREATE TABLE
sentry=> grant all privileges on table sentry_email to sentry;
GRANT
sentry=> GRANT USAGE, SELECT ON SEQUENCE sentry_email_id_seq1 to sentry;
GRANT
Nov 16
与某供应商对接的时候,要求用他们的RSA公钥加密,抛过来一个 RSAUtil.java ,核心代码大概是这样的:

public byte[] encrypt(byte[] data, PublicKey pk) throws Exception {
    Cipher cipher = Cipher.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
    cipher.init(Cipher.ENCRYPT_MODE, pk);
    byte[] raw = new byte[128]; //cipher.getBlockSize() = 128
    cipher.doFinal(data, 0, data.length, raw, 0);
    return raw;
}


RSA什么的,用php来做不要太简单,顺手就能写出来:

function rsa_encrypt($plain_text, $public_key_path)
{
    $public_key = openssl_pkey_get_public(file_get_contents($public_key_path));
    openssl_public_encrypt($plain_text, $encrypted, $public_key);
    return bin2hex($encrypted);
}

function rsa_decrypt($encrypted, $private_key_path)
{
    $private_key = openssl_pkey_get_private(file_get_contents($private_key_path));
    openssl_private_decrypt(hex2bin($encrypted), $plain_text, $private_key);
    return $plain_text;
}


结果果然不行,发过去以后,对方表示无法解密,而我没有对方的私钥,不好验证,没办法,只能自己动手,造一对rsa密钥:

引用
$ openssl genrsa -out test.pem 1024
$ openssl rsa -in test.pem -pubout -out test_public.pem


试了下,确实和 java 的结果不互通。不过在测试过程中发现一个现象:java生成的加密串总是一样的,而php生成的加密串总是不一样的。google搜了一下"php openssl_public_encrypt different everytime", Stack Overflow 的解释是,PHP的 openssl_public_encrypt 默认使用 PKCS#1 算法,引入随机数,用于防止流量探测(频率分析、密文匹配什么的,我就不懂了)。

所以很显然,Bouncy Castle 没有使用 PKCS#1 算法,放狗搜到官方文档说,Cipher.getInstance("RSA", "BC") ,第一个参数 RSA 相当于 "RSA/NONE/NoPadding" (当然也可以指定 RSA/NONE/PKCS1Padding )。

看了下 php的openssl_public_encrypt文档,可以给第四个参数“padding”指定不同的值,例如 OPENSSL_NO_PADDING ,但是试了下,发现直接失败了,只好再放狗,竟然搜到了 php的bugreport,还好第一个回复就说明了原因:需要手动用 ASCII 0 填充到 blocksize 才行(当然rsa并不禁止使用其他value,主要是加解密双方要约定好)。

验证了一下,用 OPENSSL_NO_PADDING  能够正常解密 java 生成的密文,并且在明文前面填充了若干 ASCII 0 ,补全到128字节,就此解决问题:

function rsa_encrypt($plain_text, $public_key_path)
{
    $public_key = openssl_pkey_get_public(file_get_contents($public_key_path));
    openssl_public_encrypt(str_pad($plain_text, 128, "\0", STR_PAD_LEFT), $encrypted, $public_key, OPENSSL_NO_PADDING);
    return bin2hex($encrypted);
}

function rsa_decrypt($encrypted, $private_key_path)
{
    $private_key = openssl_pkey_get_private(file_get_contents($private_key_path));
    openssl_private_decrypt(hex2bin($encrypted), $plain_text, $private_key, OPENSSL_NO_PADDING);
    return ltrim($plain_text, "\0");
}


分页: 1/7 第一页 1 2 3 4 5 6 7 下页 最后页 [ 显示模式: 摘要 | 列表 ]