月度归档:2015年01月

bash的陷阱(2): 函数返回值问题

一个脚本里遇到的,这个函数的写法是:

inner_stop(){
  local pid=$( get_service_pid )
  [ ! -z "$pid" ] && kill $pid
}

在调用完这个函数的地方需要检查一下它的执行结果,通过 $? 来判断,结果当前一句pid为空时,inner_stop 总会返回1。本来的意图是,判断当pid存在才执行kill,否则pid为空直接返回成功,而不是返回错误。这里忘了bash函数的返回值是上最后一条语句的执行结果 [ xxx ] 测试语句不满足时返回1

在返回值这点上要注意,不同于其他语言,bash属于弱类型语言,并且返回值总是最后一个命令的执行结果,if condition; then doSomething; fi 语句即可以返回if condition 的执行结果,也可以返回doSomething的结果。(注意这里说的结果是指return value,不是标准输出的内容)

#!/bin/bash

foo() {
  [ ! -z "$x" ] && echo "x not empty"
}

foo || echo "foo function return $?"

上面脚本执行时,如果没有全局声明过x变量,foo函数的返回总是1(错误),需要对另一个分支显式的return 0才行。

事先张扬的马拉松计划

以前就有过想法参加一次长城马拉松,前几天在42trip上看到这个赛事,犹豫了几天,发现已经错失了所有上半年可报的马拉松赛程,还是决定报名了金山岭长城马拉松(半程)。

看到有人评论,这个难度比较大,即使是半程也怀疑自己现在的状态是否能跑下来,因为身体的原因,已经半年多的时间没有跑步了,从现在算起还有11周时间。不管到时能否跑下来,不过至少这件事可以促使自己恢复跑步。

希望今年可以参加至少2次半程马拉松,上半年和下半年各一次。

执行情况:
2015.1.25 10公里,70分钟左右
2015.2.1 12公里,85分钟(下雪天)
2015.2.8 12公里,82分钟
2015.2.22 5公里
2015.3.1 12公里,75分钟
2015.3.29 15公里,105分钟
2015.4.19 21公里,金山岭长城马拉松(半程) 4小时59分

一个故事

晚上看到朋友圈里转的一个“凌晨三点半,一个陌生女人的来电”一文,讲述了五十年代发生在丹麦的故事,一位敏感而坚定的消防员救了一位在电话里说不清楚方位已经奄奄一息的女人;虽然最后文章变成了一篇招聘贴,但这个故事让我想起了两三年前还在做”来往”时遇到的一个事情。

我现在已经想不起这件事发生在2011年还是2012年(原帖已经找不到),那时候整个团队人不多,在阿里滨江园区用前CEO的办公室做项目室。当时来往还没有大规模推广用户量很少。有天晚上一个用户的帖子引起了大家的关注,原话大概是:老公我身上的血已经不多,请你快点回来。

看到这个帖子时,我感觉恶作剧的成分更多。后来是高倩同学说她直觉认为这不是一个玩笑;问有什么办法可以查到这个用户的联系方式,因为大家在这个帖子下面的回复劝阻并没能让这个用户再发言跟大家对话。

当时已经很晚,整个项目室就我一个人(那段时间习惯晚来晚走),我查到了这个用户的ip和手机号码,跟大家讨论后决定先打电话给用户问问情况。

在拨这个用户的手机号码时,非常不安,脑海中浮想过各种自杀的场景。电话铃声响了很久,我一边在构思该用怎样的话去挽留一个自杀的人,一边在想想对方可能是一个怎样的人:她可能是一个精神失常的人,该怎么对话?她可能已经奄奄一息?是在浴缸里用刀片割伤自己的手腕,血已染红整个浴缸?她是一个普通的家庭主妇,为了要挽留提出离婚的丈夫?

在我的不安和各种浮想联翩之后,电话超时,没人接听;过几分钟再打依然无人接听。这让我既感到轻松又感到失落,臆想的场景一个都没发生。很像电影《功夫》里周星驰去非正常人类研究中心请火云邪神时,想象着打开那扇门里面是如何的恐怖和血腥,打开后看到的却是一个普通的老人在安静地看着报纸。

拨打用户手机不通之后,我们选择了报警。用户的ip和手机号所在地并不一致,印象中一个是萍乡,一个是九江。先是打了IP所在地的110,接听电话的警官听完了我的陈述后,认为这是一件网络事件,并不属于刑事案件,不在他们管辖范围之内,给了我一个专门负责处理网络舆情的电话号码。我忘记了当时再打过去后,是没有打通还是对方仅仅是做了一个记录。

当时除了来往的同学,也有一些其他同事很关注这件事;在我打给IP地所在的公安局时,负责阿里邮箱的盛广也打给了手机号所在地区的公安局。后来还是公安机关联系到了他们,这个女的并未自杀,只是想引起她老公的重视。

事情过去好几年了,细节丢失了很多,大家权当个故事吧。

bash的陷阱(1): 逻辑或碰到local修饰符的情况

bash的函数如果有返回值,并要赋值给一个变量,跟普通的命令调用没有差异,都是: v=$(func) 的方式。需要注意的是,这里执行了fork操作,如果函数里有调用exit的话,只是让子进程退出,当前进程并不会退出。如果想要让当前进程也退出,可以使用set -e或在函数调用结束的时候再判断一次,比如:

v=$(func) || exit $?

如果函数调用失败,当前进程也退出。

今天遇到一个跟这个相关的陷阱,但并不是fork的问题,而是bash语法解析的问题,先看看我当时的写法,脚本简化后:

$ cat a.sh
#!/bin/bash

foo() {
    echo "error" && exit 1
}

bar() {
    local r=$( foo ) || exit $?
    echo $r # 执行了这里
}

bar

如上,函数bar以fork的方式调用foo,我们预期是foo返回值不为0时,bar函数也退出。所以上面bar函数里最后的echo $r不应该被执行到。但实际运行的时候发现这段代码确实可以执行到bar函数的最后一行。

后来,去掉了local修饰符,执行符合预期了:

bar() {
    r=$( foo ) || exit $?
    echo $r # 不会执行到这里
}

看来是 r=$( foo ) || exit $? 里的逻辑或||被解析器作为等号后边两个命令的逻辑运算;而前边加了local修饰符之后,逻辑或||被当成表达式local r=$(foo) 与 命令 exit 之间的逻辑运算。

vpn脚本的改进

感谢巨石的验证和建议,之前小看了云梯vpn的规模,以为他们只有那么几台服务器,所以在脚本里把域名写死了;后来发现不同用户的服务域名还是不一样的,改进了一下脚本,不局限于云梯的vpn,适用所有的vpn;代码在这里

安装方式:

$ mkdir -p ~/bin
$ curl -s http://hongjiang.info/vpn.sh > ~/bin/vpn

选项:

$ vpn
Usage: /Users/hongjiang/bin/vpn {list|ping|optimal|conn|close|status}

自动连接:

$ vpn conn
ok, jp3.vpnplease.com connected.

可以指定连接协议:

$ vpn conn L2TP #或PPTP
ok, jp2.vpnplease.com connected.

断开连接:

$ vpn close
ok, jp3.vpnplease.com disconnected.

查看vpn列表(名称:地址):

$ vpn list
VPN (PPTP): jp2.vpnplease.com
jp3.vpnplease.com: jp3.vpnplease.com
us1.vpnplease.com: us1.vpnplease.com
...

查看所有vpn的状态:

$ vpn status

* (Disconnected)   2599A4CB-7D76-4878-8FAF-7496E5F6C58F PPP --> L2TP       "us2.vpnplease.com"              [PPP:L2TP]
* (Disconnected)   57A3276C-505F-4E13-B402-0CB6EC5C9AD7 PPP --> PPTP       "VPN (PPTP)"                     [PPP:PPTP]
* (Connected)      90784ECA-DCFA-4197-B2E5-FBE84AFB9C00 PPP --> L2TP       "jp3.vpnplease.com"              [PPP:L2TP]
...     

获取最优的服务名称(协议参数可选):

$ vpn optimal pptp  #或 l2tp
VPN (PPTP)

测试连接速度:

$ vpn ping
jp2.vpnplease.com  52.190
jp1.vpnplease.com  53.681
sg2.vpnplease.com  171.482
...

选择网速最快的vpn进行连接的脚本

下午github上下载速度只有几K,用了代理也不到10K,后来发现所用的vpn服务器选择了一个慢的。写了这段脚本,用来从可用的vpn选出最快的那个。脚本只适用于mac,并且前提是你已创建好了所有可选的vpn,如图:

脚本内容:

#!/bin/bash

PROG_NAME=$0
ACTION=$1

DOMAIN="vpnplease.com"
declare -a OPTION=("jp" "us" "sg")

usage() {
  echo "Usage: $PROG_NAME {test|conn|close|status}"
  exit 1
}

vpn_test() {

  for c in "${OPTION[@]}";do
    for i in `seq 1 3`; do
      proxy="$c$i.$DOMAIN"
      ping -c3 $proxy 2>/dev/null | tail -1 | cut -d'=' -f2 | cut -d'/' -f2 | xargs -I {} echo "$proxy " {} 2>/dev/null &
    done
  done

  children=$(jobs -p | xargs)
  trap 'kill $children >/dev/null 2>&1' SIGINT SIGTERM
  wait
}

get_fastest() {
  vpn_test 2>/dev/null | sort -nk2 | head -1 | awk '{print $1}'
}

vpn_status() {
  for c in "${OPTION[@]}";do
    for i in `seq 1 3`; do
      proxy="$c$i.$DOMAIN"
      scutil --nc status $proxy 2>/dev/null | head -1 | xargs -I {} echo "$proxy " {}
    done
  done
}

vpn_connect() {
  local name=$1

/usr/bin/env osascript <<-EOF
  tell application "System Events"
    tell current location of network preferences
      set VPN to service "$name" -- your VPN name here
      if exists VPN then connect VPN
      repeat while (current configuration of VPN is not connected)
        delay 1
      end repeat
    end tell
  end tell
EOF
}

vpn_disconnect() {
  local name=$(vpn_status | awk '$2~/Connected/{print $1}')
  [ -z $name ] && return 0

/usr/bin/env osascript <<-EOF
  tell application "System Events"
    tell current location of network preferences
      set VPN to service "$name" -- your VPN name here
      if exists VPN then disconnect VPN
    end tell
  end tell
  return
EOF
}

case "$ACTION" in
    test)
        vpn_test
    ;;
    conn)
        vpn_connect $(get_fastest)
    ;;
    close)
        vpn_disconnect
    ;;
    status)
        vpn_status
    ;;
    *)
        usage
    ;;
esac

使用方式:

1) 自动选择最优的vpn进行连接

$ vpn conn

2) 查看连接状态

$ vpn status

jp1.vpnplease.com  Connected
jp2.vpnplease.com  Disconnected
jp3.vpnplease.com  Disconnected
us1.vpnplease.com  Disconnected
us2.vpnplease.com  Disconnected
us3.vpnplease.com  Disconnected
sg1.vpnplease.com  Disconnected
sg2.vpnplease.com  Disconnected

3) 测试网速,时间最小的最优

$ vpn test

jp2.vpnplease.com  76.314
jp1.vpnplease.com  75.793
jp3.vpnplease.com  136.161
sg1.vpnplease.com  169.494
us3.vpnplease.com  242.685
us2.vpnplease.com  249.032
us1.vpnplease.com  266.427
sg2.vpnplease.com  168.750

4) 断开连接

$ vpn close 

第五权力

对于这部电影的中文名《危机解密》大部分人是比较熟知的,实际它的英文名是《The Fifth Estate》翻译为中文是“第五权力(也有翻译为第五阶层)”,这部电影我从一个同事的移动硬盘里拷贝过来,看到“第五阶层”这样的名字根本不知道是讲什么,打开预览了一下才知道是关于阿桑奇的。

在这部电影里也解释了一下什么是第五权力,是对”第四权力“的继承,wiki里也有解释:

第五权力指由互联网或网民所组成、对社会甚至政府的网络监察,其代表了不被“较狭窄、单向领域(one-way scope)的第四权传播媒体”所包含的一种新社会大众媒体。

我关注这部电影,可能还有部分是偏Geek的原因,了解一下这些家伙们是怎么干活的。这张截图是电影里的一个画面,这段代码看上去像是perl,google了一下发现在reddit有个讨论,被确认是一段lua代码。在imgur上有更多的代码截图。尽管这些代码是在演戏,但算是比较认真的了,有些代码片段里有涉及tor,还有删除服务器资料的时候是敲了 rsync –delete 至少没有随意糊弄观众。

对于这部电影本身来说,有些争议,因为他主要出自阿桑奇的搭档(后来两人闹翻了)的一本书,或许另外两部有关危机解密的纪录片会更客观一些:《WikiRebels: The Documentary》 , 《We Steal Secrets: The Story of WikiLeaks》,在纪录片的图片里发现一张印有阿桑奇头像和一段乔治.奥威尔的话的涂鸦墙。

During times of universal deceit, telling the truth becomes a revolutionary act

用strings命令查看kafka-log内容

kafka的log内容格式还不没怎么了解,想快速浏览消息内容的话,除了使用它自带的kafka-console-consumer.sh脚本,还可以直接去看log文件本身,不过内容里有部分二进制字符,通过命令看的话会有乱码。

strings 命令可以过滤掉二进制编码,但默认它也会过滤掉中文字符,只留有英文字符。要用它的-e S参数可以同时过滤出中文或英文字符,但仍会包含了小部分的二进制编码,可以在通过iconv去掉一下,能大致看到消息内容:

$ cat log-strings.sh
#!/bin/bash

PROG_NAME=$0
LOG_FILE=$1

if [ -z "$LOG_FILE" ];then
  echo "Usage: $PROG_NAME logfile"
  exit 1
fi

strings -e S "$LOG_FILE" | iconv -c -f "UTF-8" -t "UTF-8"

再谈随机数引起的阻塞问题

Java的随机数实现有很多坑,记录一下这次使用jdk1.8里新增的加强版随机数实现SecureRandom.getInstanceStrong() 遇到的问题。

之前在维护ali-tomcat的时候曾发现过jvm随机数算法选用不当导致tomcat的SessionID生成非常慢的情况,可以参考JVM上的随机数与熵池策略Docker中apache-tomcat启动慢的问题 这两篇文章。不过当时没有太追究,以为使用了-Djava.security.egd=file:/dev/./urandom就可以避免了,在这次项目里再次遇到随机数导致所有线程阻塞之后发现这块还挺多规则。

本次项目中使用的是jdk1.8,启动参数里设置了

-Djava.security.egd=file:/dev/./urandom

使用的随机数方式是Java8新增的:

SecureRandom.getInstanceStrong();

碰到故障时,线程阻塞在

"DubboServerHandler-xxx:20880-thread-1789" #28440 daemon prio=5 os_prio=0 tid=0x0000000008ffd000 nid=0x5712 runnable [0x000000004cbd7000]
java.lang.Thread.State: RUNNABLE
    at java.io.FileInputStream.readBytes(Native Method)
    at java.io.FileInputStream.read(FileInputStream.java:246)
    at sun.security.provider.NativePRNG$RandomIO.readFully(NativePRNG.java:410)
    at sun.security.provider.NativePRNG$RandomIO.implGenerateSeed(NativePRNG.java:427)
    - locked <0x00000000c03a3c90> (a java.lang.Object)
    at sun.security.provider.NativePRNG$RandomIO.access$500(NativePRNG.java:329)
    at sun.security.provider.NativePRNG$Blocking.engineGenerateSeed(NativePRNG.java:272)
    at java.security.SecureRandom.generateSeed(SecureRandom.java:522)

因为这个地方有加锁,locked <0x00000000c03a3c90>,所以其它线程调用到这里时会等待这个lock:

"DubboServerHandler-xxx:20880-thread-1790" #28441 daemon prio=5 os_prio=0 tid=0x0000000008fff000 nid=0x5713 waiting for monitor entry [0x000000004ccd8000]
java.lang.Thread.State: BLOCKED (on object monitor)
    at sun.security.provider.NativePRNG$RandomIO.implGenerateSeed(NativePRNG.java:424)
    - waiting to lock <0x00000000c03a3c90> (a java.lang.Object)
    at sun.security.provider.NativePRNG$RandomIO.access$500(NativePRNG.java:329)
    at sun.security.provider.NativePRNG$Blocking.engineGenerateSeed(NativePRNG.java:272)
    at java.security.SecureRandom.generateSeed(SecureRandom.java:522)

去查 NativePRNG$Blocking代码,看到它的文档描述:

A NativePRNG-like class that uses /dev/random for both seed and random material. Note that it does not respect the egd properties, since we have no way of knowing what those qualities are.

奇怪怎么-Djava.security.egd=file:/dev/./urandom参数没起作用,仍使用/dev/random作为随机数的熵池,时间久或调用频繁的话熵池很容易不够用而导致阻塞;于是看了一下 SecureRandom.getInstanceStrong()的文档:

Returns a SecureRandom object that was selected by using the algorithms/providers specified in the securerandom.strongAlgorithms Security property.

原来有自己的算法,在 jre/lib/security/java.security 文件里,默认定义为:

securerandom.strongAlgorithms=NativePRNGBlocking:SUN

如果修改算法值为NativePRNGNonBlocking:SUN的话,会采用NativePRNG$NonBlocking里的逻辑,用/dev/urandom作为熵池,不会遇到阻塞问题。但这个文件是jdk系统文件,修改它或重新指定一个路径都有些麻烦,最好能通过系统环境变量来设置,可这个变量不像securerandom.source属性可以通过系统环境变量-Djava.security.egd=xxx来配置,找半天就是没有对应的系统环境变量。只好修改代码,不采用SecureRandom.getInstanceStrong这个新方法,改成了SecureRandom.getInstance("NativePRNGNonBlocking")

对于SecureRandom的两种算法实现:SHA1PRNGNativePRNGsecurerandom.source 变量的关系,找到一篇解释的很清楚的文章:Using the SecureRandom Class

On Linux:

1) when this value is “file:/dev/urandom” then the NativePRNG algorithm is registered by the Sun crypto provider as the default implementation; the NativePRNG algorithm then reads from /dev/urandom for nextBytes but /dev/random for generateSeed

2) when this value is “file:/dev/random” then the NativePRNG algorithm is not registered by the Sun crypto provider, but the SHA1PRNG system uses a NativeSeedGenerator which reads from /dev/random.

3) when this value is anything else then the SHA1PRNG is used with a URLSeedGenerator that reads from that source.

4) when the value is undefined, then SHA1PRNG is used with ThreadedSeedGenerator

5) when the code explicitly asks for “SHA1PRNG” and the value is either “file:/dev/urandom” or “file:/dev/random” then (2) also occurs

6) when the code explicitly asks for “SHA1PRNG” and the value is some other “file:” url, then (3) occurs

7) when the code explicitly asks for “SHA1PRNG” and the value is undefined then (4) occurs

至于SHA1PRNG算法里,为何用urandom时,不能直接设置为file:/dev/urandom而要用变通的方式设置为file:///dev/urandom或者 file:/dev/./urandom,参考这里

In SHA1PRNG, there is a SeedGenerator which does various things depending on the configuration.

  1. If java.security.egd or securerandom.source point to “file:/dev/random” or “file:/dev/urandom”, we will use NativeSeedGenerator, which calls super() which calls SeedGenerator.URLSeedGenerator(/dev/random). (A nested class within SeedGenerator.) The only things that changed in this bug was that urandom will also trigger use of this code path.

  2. If those properties point to another URL that exists, we’ll initialize SeedGenerator.URLSeedGenerator(url). This is why “file:///dev/urandom”, “file:/./dev/random”, etc. will work.