分类目录归档:linux

进程被debug时的状态

在mac/bsd和linux上ps展示进程状态的字符与linux可能含义不同,比如我的java进程在mac上ps看到是"TX+"状态

➜  ps aux | grep "[j]ava"
hongjiang  60387  0.0  0.4  6072548  36516 s001  TX+  7:28PM  0:00.19 java Daemon

Linux上man ps看到 X 表示 "dead (should never be seen)", 而Mac/BSD下则是 “The process is being traced or debugged” 的含义。 Linux上当一个进程被debug中,状态跟stopped一样都使用T表示(在某些高版本linux内核里会区分开,使用小写t表示进程是被debugger跟踪)

通过strace查看一个进程的标准输出内容

线上某个业务java进程出了一些麻烦,去诊断的时候发现jstack无法输出,可能是jdk或os版本的问题。这时还可以尝试一下kill -3,它默认会输出到进程的标准输出。如果不幸这个进程的标准输出被重定向到了 /dev/null 或者重定向到某个文件,但却因为很多其他日志也在大量的输出,导致日志文件过大,要从中找出线程栈相关的日志,还要耗点时间;这个时候,可以通过strace来跟踪一个进程的标准输入。

strace输出的信息需要一些处理,可以通过管道与其他命令组合(通过-o参数或-ff参数)

这里 -o 参数后边是一个字符串表示输出文件,如果字符串开头是一个"|"会被strace识别为管道

$ strace -f -e trace=write -e verbose=none -e write=1,2 -q -p $pid -o "| grep '^ |' | cut -c11-60 | sed -e 's/ //g' | xxd -r -p"

或者 -ff 参数,用管道与其他命令组合,注意strace的错误输出也要重定向

$ strace -ff -e trace=write -e write=1,2 -s 1024 -q -p $pid 2>&1 | cut -c11-60 | sed -e 's/ //g' | xxd -r -p

不过这两种方式都遇到一个问题,因为buffer的问题导致管道后边的命令没能完全处理strace跟踪到的数据(要等到后续的数据塞满buffer),有点像grep需要--line-buffer解决缓冲区问题,但不知道这里有什么方式,尝试过stdbuf也没有解决。最后只能分两步,先把strace的内容输出到文件,然后再对内容解析:

$ strace -f -e trace=write -e write=1,2 -q -p $pid  -o /tmp/slog

$ grep " |" /tmp/slog | cut -c11-60 | sed -e 's/ //g' | xxd -r -p

tmux下显示ssh目标host的问题

当在tmux下ssh到远程机器时,希望显示远程host,google后找到了一段对ssh封装过的函数:

ssh() {
    if [ "$(ps -p $(ps -p $$ -o ppid=) -o comm=)" = "tmux" ]; then
        tmux rename-window "$*"
        command ssh "$@"
        tmux set-window-option automatic-rename "on" 1>/dev/null
    else
        command ssh "$@"
    fi
}

这段函数在我的mac上运行效果正常,如下图

但当我在一台linux上运行时,ctrl-d退出远程ssh时,会一直卡在退出状态,不管ctrl-c或什么操作也无法回到原bash,如下图

此时只通过tmux的kill-window的操作方式来关掉这个窗口,而在mac下是没有这个问题的,分析了一下,发现在我的mac上,tmux被oh-my-zsh给alias成了一个_zsh_tmux_plugin_run函数:

➜  which ssh
ssh () {
    if [ "$(ps -p $(ps -p $$ -o ppid=) -o comm=)" = "tmux" ]
    then
        _zsh_tmux_plugin_run rename-window "$*"
        command ssh "$@"
        _zsh_tmux_plugin_run set-window-option automatic-rename "on" 1>/dev/null
    else
        command ssh "$@"
    fi
}

_zsh_tmux_plugin_run又做了一些环境变量的判断和处理,猜测可能是因为这些环境变量的差异所导致,先去掉tmux set-window-option automatic-rename "on" 这句最后对标准输出的重定向,看看输出什么信息,结果在修改之后却可以正常了:

还不太清楚为何这个重定向导致了ssh退出后没法回到原shell下,先记录下这个现象。

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

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.

查看多个节点上的日志:multitail脚本

日志分布在多个节点上,想要实时查看多个日志输出的话可以用这个脚本。这个脚本模拟了multitail的效果:

$ cat multitail.sh

#!/bin/bash
set -f

PROG_NAME=$0

usage() {
    echo "Usage: $PROG_NAME ip1,ip2,ip3... file1 file2 file3 ..."
    exit 1
}

if [ $# -lt 2 ]; then
    usage
fi

COMMAND="tail -f"

IP_LIST=$1
shift && FILES=("$@")

for file in ${FILES[*]}; do
    COMMAND="$COMMAND $file"
done

SED="sed"
if [[ $OSTYPE == *darwin* ]]; then
  which gsed 
  if [ $? -eq 0 ];then
    SED="gsed"
  else
    echo "mac os need gsed, please install gnu-sed." 
    exit 1
  fi
fi

for ip in $(echo "$IP_LIST" | tr ',' '\n'); do
  if [ ${#FILES[@]} -gt 1 ];then
    ssh user@$ip "$COMMAND" | $SED 's/\(==> \)/\1'"$ip:"'/' &
  else
    #ssh user@$ip "$COMMAND" | $SED '0~10a===='"$ip"'====' &
    ssh user@$ip "$COMMAND" | $SED 's/^/'"$ip "'/' &
  fi
done

CHILD_PIDS=$(ps -ef | grep $$ | grep -v grep | awk '$3=='"$$"'{print $2}' | xargs)

# CTRL-C to stop
trap 'kill $CHILD_PIDS >/dev/null 2>&1' SIGINT SIGTERM 
wait

使用方式:

$ ./multitail.sh 192.168.10.1,192.168.10.2 /data/app1/a.log /data/app2/b.log

可以配合ack命令对一些关键字高亮:

$ ./multitail.sh ip1,ip2 log1 log2 | ack --passthru login

注意,依赖ssh执行远程命令,所以前提是执行脚本的机器必须与目标ip打通ssh,不需要密码。

推荐一个更好用的diff命令:icdiff

icdiff王福强推荐的一个很酷的可以用来替代系统默认的diff工具的命令行比较工具,效果跟filemeger这些有的一比,关键是在shell下使用很方便。我现在已经用它替代默认的diff,并替代了git diff的效果,如下:

在 .gitconfig 里添加

【diff】#防止模板渲染转义,请替换为英文的中括号
    external = git-diff-wrapper.sh

脚本内容:

$ cat git-diff-wrapper.sh
#!/bin/bash
/usr/local/bin/icdiff $2 $5

想要忽略这个外部diff而使用官方的diff效果的话:

$ git diff --no-ext-diff

一次编码问题的排查

上周遇到的一个问题,代码里有通过scala.io.Source.fromFile 方式读取文件内容,并且没有指定编码格式,开始程序运行是ok的,后来添加了中文,发现在某些情况下会遇到乱码问题,并不是必然发生的,而是不同的人ssh登录到linux的测试环境启动应用可能会有不同的结果。这里记录一下当时的情况。

问题的排查,首先确认Source.fromFile这个api里面对编码的使用方式:

def fromFile(name: String)(implicit codec: Codec): BufferedSource =
    fromFile(new JFile(name))(codec)

不指定编码的话,系统使用了一个隐式参数作为默认编码,看一下它是什么:

scala>  def f(implicit c: io.Codec) = c
f: (implicit c: scala.io.Codec)scala.io.Codec

scala> f.name
res6: String = US-ASCII

这台linux上显示的是US-ASCII,而在我的mac上是UTF-8,看来是编码设置的问题,这个值应该是jvm的系统属性里编码相关的:

scala> System.getProperties.foreach(s => if (s.toString.indexOf("enc") != -1) println(s) )
(file.encoding.pkg,sun.io)
(sun.jnu.encoding,ANSI_X3.4-1968)
(file.encoding,ANSI_X3.4-1968)
(sun.io.unicode.encoding,UnicodeLittle)

测试了一下是file.encoding这个系统属性,修改这个属性为UTF-8后确实可以正常的。但是另一个同事登录这台linux后运行却跟我的情况不同,他登录后(使用同一账号)直接jvm的file.encoding默认就是UTF-8,这看起来有些奇怪。

jvm的系统属性,其实也是根据OS的环境变量得到的。虽然这台机器的LANGLC_*等指定了UTF-8,但并不意味jvm的file.encoding也一样:

$ locale
LANG=en_US.UTF-8
LC_CTYPE=UTF-8
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=

要确认file.encoding属性的话,可以使用locale charmap命令,我指定的结果显示:

$ locale charmap
ANSI_X3.4-1968

ANSI_X3.4-1968是系统默认的编码,而另一个同事显示的是”UTF-8″,又把怀疑落在ssh客户端上。我是在mac下直接通过ssh命令的方式登录,而他则是在windows下使用putty登录。各自打开ssh的 -verbose参数:

$ ssh -v xxx@ip

他的展示信息里出现了:

debug1: Sending env LC_ALL = en_US.UTF-8 
debug1: Sending env LANG = zh_CN.UTF-8 

而我的ssh客户端则没有这些信息,确实是ssh客户端配置所引起的差异。(关于LC_*LC_ALL等环境变量的关系后续有精力再整理)

检测最耗cpu的线程的脚本

这个脚本用于定位出当前java进程里最耗cpu的那个线程,给出cpu的占用率和当前堆栈信息。这个脚本仅限于linux上,我没有找到在mac下定位线程使用cpu情况的工具,如果你知道请告诉我一下。

先模拟一个耗cpu的java进程,启动一个scala的repl并在上面跑一段死循环:

scala> while(true) {}

脚本执行效果:

$ ./busythread.sh `pidof java`
tid: 431  cpu: %98.8
"main" prio=10 tid=0x00007f777800a000 nid=0x1af runnable [0x00007f7781c2e000]
    java.lang.Thread.State: RUNNABLE
    at $line3.$read$$iw$$iw$.<init>(<console>:8)
    at $line3.$read$$iw$$iw$.<clinit>(<console>)
    at $line3.$eval$.$print$lzycompute(<console>:7)
    - locked <0x00000000fc201758> (a $line3.$eval$)
    at $line3.$eval$.$print(<console>:6)
    at $line3.$eval.$print(<console>)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.call(IMain.scala:739)
    at scala.tools.nsc.interpreter.IMain$Request.loadAndRun(IMain.scala:986)
    at scala.tools.nsc.interpreter.IMain$WrappedRequest$$anonfun$loadAndRunReq$1.apply(IMain.scala:593)
    at scala.tools.nsc.interpreter.IMain$WrappedRequest$$anonfun$loadAndRunReq$1.apply(IMain.scala:592)
    at scala.reflect.internal.util.ScalaClassLoader$class.asContext(ScalaClassLoader.scala:31)
    at scala.reflect.internal.util.AbstractFileClassLoader.asContext(AbstractFileClassLoader.scala:19)
    at scala.tools.nsc.interpreter.IMain$WrappedRequest.loadAndRunReq(IMain.scala:592)
    at scala.tools.nsc.interpreter.IMain.interpret(IMain.scala:524)
    at scala.tools.nsc.interpreter.IMain.interpret(IMain.scala:520)

脚本内容:

#!/bin/bash

if [ $# -eq 0 ];then
    echo "please enter java pid"
    exit -1
fi

pid=$1
jstack_cmd=""

if [[ $JAVA_HOME != "" ]]; then
    jstack_cmd="$JAVA_HOME/bin/jstack"
else
    r=`which jstack 2>/dev/null`
    if [[ $r != "" ]]; then
        jstack_cmd=$r
    else
        echo "can not find jstack"
        exit -2
    fi
fi

#line=`top -H  -o %CPU -b -n 1  -p $pid | sed '1,/^$/d' | grep -v $pid | awk 'NR==2'`

line=`top -H -b -n 1 -p $pid | sed '1,/^$/d' | sed '1d;/^$/d' | grep -v $pid | sort -nrk9 | head -1`
echo "$line" | awk '{print "tid: "$1," cpu: %"$9}'
tid_0x=`printf "%0x" $(echo "$line" | awk '{print $1}')`
$jstack_cmd $pid | grep $tid_0x -A20 | sed -n '1,/^$/p'

脚本已放到服务器上,可以通过下面的方式执行:

$ bash <(curl -s http://hongjiang.info/busythread.sh)  java_pid

update: 感谢容若的反馈,很多环境的procps版本较低,top还不支持-o参数,排序那块用sort解决了,脚本已更新。

mac上通过ssh连接docker的container

下午尝试了一下在mac上通过ssh的方式连接到container,因为之前采用交互方式启动container,一个实例只有一个交互终端,有时需要多个终端只能通过screen一类的工具,而screen有个不爽的地方是它的屏幕缓冲,不能方便的通过滚屏的方式看之前输出内容。还是通过多个终端ssh到container上比较方便。

在mac上docker依赖virtualbox,想要与container之间通讯,需要两层端口映射,一层是在mac与virtualbox之间,另一层是在宿主机与container之间。

在宿主与container之间的端口映射,docker命令中就有参数支持,使用-p参数:

$ docker run -d -p 2222:22 ubuntu /usr/sbin/sshd -D

上面以daemon的方式启动一个ubuntu的container实例,在启动时运行了sshd,并把container的22端口映射到了宿主的2222端口。

现在需要再把mac与宿主的2222端口打通,需要通过virtualbox的命令来设置(需要先boot2docker stop):

$ VBoxManage modifyvm "boot2docker-vm" --natpf1 "containerssh,tcp,,2222,,2222"

上面的命令把virtualbox里的宿主机2222端口映射到了本地的2222,现在可以通过访问mac本地的2222端口来连接container实例了:

$ ssh root@localhost -p2222

注意,Ubuntu和CentOS的openssh-server默认配置都是禁止root用户通过ssh访问的,需要的话可以去修改ssh-server的配置。

如果想要修改virtualbox的映射,可以删除之前的映射规则,不记得映射规则名称的话,可以通过showvminfo的信息grep一下

$ VBoxManage showvminfo boot2docker-vm  | grep 2222
NIC 1 Rule(0):   name = containerssh, protocol = tcp, host ip = , host port = 2222, guest ip = , guest port = 2222

然后通过name删除这条规则:

$ VBoxManage controlvm boot2docker-vm natpf1 delete "containerssh"