月度归档:2014年09月

goto,一个用于快速切换目录的脚本

在shell下经常在不同的目录之间切换的话,希望有一个更好用的cd或pushd/popd命令。以前曾尝试过autojump, asdf 等工具,发现这些工具有些“重”了,它们背后通过ruby或python记录你每次cd的路径,统计出每个路径的频率,算出一个权重,根据权重优先来匹配跳转。我希望有一个更轻的(不用依赖ruby或python),不用那么“聪明”的,能够记录最近的路径并可以方便跳转过去的工具。

我的需求是列出之前切换过的10个目录,用序号记录,按时间排序,最近一次切换的目录排在第一位,效果如下:

$ dirs
 1  /data/server/tomcat
 2  /System/Library/Frameworks/JavaVM.framework/Versions/jdk1.8
 3  /data/work/opensources/openjdk
 4  /tmp/a b
 5  /data/work/opensources/apache
 6  /tmp
 7  /Users/hongjiang
 8  /Applications
 9  /data/downloads
10  /data/tools

当我想要切换到某个目录时,只需要命令后边跟序号就可以,如下:

 $ goto 2
 $ pwd
 /System/Library/Frameworks/JavaVM.framework/Versions/jdk1.8
 $ goto 9
 $ pwd
 /data/downloads

这个效果可以通过几个函数来实现,首先是要把cd命令alias为pushd,这样每次切换目录都会压栈:

pushd() {
    if [ $# -eq 0 ]; then
        DIR="${HOME}"
    else
        DIR="$1"
    fi

    builtin pushd "${DIR}" > /dev/null
}

alias cd='pushd'

然后对内置的dirs命令进行包装:

dirs() {
    tmphash="/tmp/.dirs"

    builtin dirs -plv |
    awk -F'\t' '{
        if (a[$2]=="") a[$2]=$1;
        if (length(a)==10) exit
    }END{for(i in a) print a[i],i}' |
    sort -nk1 | cut -d' ' -f2- > $tmphash

    nl $tmphash
}

这样在每次执行dirs函数时,会通过内置的dirs命令过滤最近10次切换过的目录并保持在”/tmp/.dirs”文件里。最后实现goto函数:

goto() {
    tmphash="/tmp/.dirs"
    if [ ! -f $tmphash ]; then
        echo "no record"
        return 1
    fi
    if [ $# -eq 0 ]; then
        return 1
    fi

    dest=`awk -v n=$1 'NR==n&&1' $tmphash`

    if [[ $dest == "~" ]]; then
        dest=${HOME}
    elif [[ $dest == "~/"* ]]; then
        dest="${HOME}/${dest:2}"
    elif [[ $dest == "~"* ]]; then
        dest=`eval "echo $dest"`
    fi

    pushd $dest
}

将上面几个函数放入.zshrc里,source一下或重启shell即可,只在zsh上测试过,还未在bash上验证。

最近看过的电影(7)

刚在影院看完了《空中营救》,前半部情节很紧凑,但后边的编剧有硬伤,不能自圆其说。很奇怪豆瓣上为何这部电影的分数还挺高。其实主要还是冲着主演”连姆·尼森”去看的。他的《Taken》系列我非常喜欢,思路清晰,动作干净利索。不过我对他印象最深的倒是一部评价不那么高的电影《人狼大战》(The Grey)

检测最耗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"

检查应用jar冲突的脚本

这个脚本用于定位应用classpath下有哪些jar包冲突,列出它们的相似度,以及冲突的class个数,执行效果如下:

$ ./cp-check.sh .
Similarity  DuplicateClasses  File1                                          File2
%100        502               jackson-mapper-asl-1.9.13.jar                  jackson-mapper-lgpl-1.9.6.jar
%100        21                org.slf4j.slf4j-api-1.5.6.jar                  slf4j-api-1.5.8.jar
%100        9                 jcl-over-slf4j-1.5.8.jar                       org.slf4j.jcl-over-slf4j-1.5.6.jar
%100        6                 org.slf4j.slf4j-log4j12-1.5.6.jar              slf4j-log4j12-1.5.8.jar
%99         120               jackson-core-asl-1.9.13.jar                    jackson-core-lgpl-1.9.6.jar
%98         513               jboss.jboss-netty-3.2.5.Final.jar              netty-3.2.2.Final.jar
%98         256               jakarta.log4j-1.2.15.jar                       log4j-1.2.14.jar
%98         97                json-lib-2.2.3.jar                             json-lib-2.4-jdk15.jar
%87         186               fastjson-1.1.15.jar                            fastjson-1.1.30.jar
%85         215               cglib-nodep-3.1.jar                            sourceforge.cglib-0.0.0.jar
%83         93                commons-beanutils-1.7.0.jar                    commons-beanutils-core-1.7.0.jar
%21         6                 commons-logging-1.1.1.jar                      org.slf4j.jcl-over-slf4j-1.5.6.jar
%21         6                 commons-logging-1.1.1.jar                      jcl-over-slf4j-1.5.8.jar
%16         18                commons-beanutils-1.7.0.jar                    commons-beanutils-bean-collections-1.7.0.jar
%04         8                 batik-ext-1.7.jar                              xml-apis-1.0.b2.jar
%02         10                commons-beanutils-core-1.7.0.jar               commons-collections-3.2.1.jar
%02         10                commons-beanutils-1.7.0.jar                    commons-collections-3.2.1.jar
See /tmp/cp-verbose.log for more details.

脚本同时会输出一个包含所有冲突的class文件:/tmp/cp-verbose.log 这个verbose文件内容大致如下,记录每个有冲突的class位于哪些jar包,定位问题时可以去查:

org/jboss/netty/util/internal/SharedResourceMisuseDetector.class
         jboss.jboss-netty-3.2.5.Final.jar,netty-3.2.2.Final.jar
org/jboss/netty/util/internal/StackTraceSimplifier.class
         jboss.jboss-netty-3.2.5.Final.jar,netty-3.2.2.Final.jar
org/jboss/netty/util/internal/StringUtil.class
         jboss.jboss-netty-3.2.5.Final.jar,netty-3.2.2.Final.jar

脚本内容:

#!/bin/bash
if [ $# -eq 0 ];then
    echo "please enter classpath dir"
    exit -1
fi

if [ ! -d "$1" ]; then
    echo "not a directory"
    exit -2
fi

tmpfile="/tmp/.cp$(date +%s)"
tmphash="/tmp/.hash$(date +%s)"
verbose="/tmp/cp-verbose.log"

declare -a files=(`find "$1" -name "*.jar"`)
for ((i=0; i < ${#files[@]}; i++)); do
    jarName=`basename ${files[$i]}`
    list=`unzip -l ${files[$i]} | awk -v fn=$jarName '/\.class$/{print $NF,fn}'`
    size=`echo "$list" | wc -l`
    echo $jarName $size >> $tmphash
    echo "$list"
done | sort | awk 'NF{
    a[$1]++;m[$1]=m[$1]","$2}END{for(i in a) if(a[i] > 1) print i,substr(m[i],2)
}' > $tmpfile

awk '{print $2}' $tmpfile |
awk -F',' '{i=1;for(;i<=NF;i++) for(j=i+1;j<=NF;j++) print $i,$j}' |
sort | uniq -c | sort -nrk1 | while read line; do
    dup=${line%% *}
    jars=${line#* }
    jar1=${jars% *}
    jar2=${jars#* }
    len_jar1=`grep -F "$jar1" $tmphash | grep ^"$jar1" | awk '{print $2}'`
    len_jar2=`grep -F "$jar2" $tmphash | grep ^"$jar2" | awk '{print $2}'`
    len=$(($len_jar1 > $len_jar2 ? $len_jar1 : $len_jar2))
    per=$(echo "scale=2; $dup/$len" | bc -l)
    echo ${per/./} $dup $jar1 $jar2
done | sort -nr -k1 -k2 |
awk 'NR==1{print "Similarity DuplicateClasses File1 File2"}{print "%"$0}'| column -t

sort $tmpfile | awk '{print $1,"\n\t\t",$2}' > $verbose
echo "See $verbose for more details."

rm -f $tmpfile
rm -f $tmphash

这个是改良过的脚本;第一次实现的时候是采用常规思路,用冒泡的方式比较两个jar文件的相似度,测试一二十个jar包的时候没有问题,找一个有180多个jar包的应用来跑的时候发现非常慢,上面改良后的脚本在我的mac上检查这个应用大概3秒左右,在linux上检测一个300个jar左右的应用4~5秒,基本上够用了。

为了兼容mac(有些命令在linux与mac/bsd上方式不同),大部分情况下采用awk来处理,不过我对awk也不太熟,只好采用逐步拼接的方式,如果通过一个awk脚本来实现或许性能可以高一些,但也比较有限,大头还是在获取jar里的class列表那块。几个tips:

  • 测试发现unzip -l要比jar tvf快出一个数量级以上
  • shell里的字符串拼接尤其是比较大的字符串拼接非常耗性能
  • mac/bsd下的sed非常难用,linux上的sed用法无法在mac上运行,跨平台脚本尽量避免sed,除非都安装的是gnu-sed
  • bash4.0版本才支持map,而实际环境还运行的还是低版本的,只能自己模拟一个map,简单的做法可以基于临时文件

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

$ bash <(curl -s http://hongjiang.info/cpcheck.sh) libdir

查询ip来源的脚本

查询ip地址的方式很多,linux里有一个geoip的工具,我通过homebrew安装过并不work(可能是网络的问题无法更新这个地址库),所以实现了一个通过ip138.com来查询的. 只在mac上测试过,mac自带的sed很不好用,我用的gnu-sed,可以通过homebrew安装:

#!/bin/sh

if [ $# -lt 1 ]; then
    echo "input ip address" && exit -1;
fi

URL="http://ip138.com/ips138.asp?ip="$1
result=`curl -s $URL | iconv -f GBK -t UTF-8 | grep "td align=\"center\"" | awk 'NR==2||NR==3'`
echo $result | gsed 's/[<|>]/\n/g' | gsed '/ul\|li\|h1\|td/d' | awk NF

测试效果:

$ ip138 8.8.8.8
您查询的IP:8.8.8.8

本站主数据:美国 Google免费DNS
参考数据一:美国
网友提交的IP:美国 Google免费DNS

攻击又来了

wordpress后台看到访问量在最近10个小时猛增,通常是遇到攻击,从日志里看到几万次的请求来自 mail.mknz.net 这个网站,应该是被黑了,成为了肉鸡来攻击其它系统。请求全部是在访问wp-login.php页面,又是尝试猜测密码。还好这里做过防御。

至少经历过好几次暴力攻击了,这个帖子记录一下被攻击的情况,下次再有的话会再更新。关于防御暴力攻击的方式可以参考这篇文章

//update 2014.10.15
攻击来源:82.196.3.83, 尝试login

//update 2015.1.12
最近很多xmlrpc的攻击,安装了一个”Disable XML-RPC Pingback”的插件后,有所缓解。

tomcat-connector的微调(5): keep-alive相关的参数

tomcat默认是开启keep-alive的,有3个相关的参数可以配置:

1) keepAliveTimeout

表示在复用一个连接时,两次请求之间的最大间隔时间;超过这个间隔服务器会主动关闭连接。默认值同connectionTimeout参数,即20秒。不做限制的话可以设置为-1.

2) maxKeepAliveRequests

表示一个连接最多可复用多少次请求,默认是100。不做限制可以设置为-1. 注意如果tomcat是直接对外服务的话,keep-alive特性可能被一些DoS攻击利用,比如以很慢的方式发生数据,可以长时间持有这个连接;从而可能被恶意请求耗掉大量连接拒绝服务。tomcat直接对外的话这个值不宜设置的太大。

3) disableKeepAlivePercentage

注意,这个参数是BIO特有的,默认情况BIO在线程池里的线程使用率超过75%时会取消keep-alive,如果不取消的话可以设置为100.

tomcat-connector的微调(4): 超时相关的参数

tomcat对每个请求的超时时间是通过connectionTimeout参数设置的。默认的server.xml里的设置是20秒,如果不设置这个参数代码里会使用60秒。

这个参数也会对POST请求有影响,但并不是指上传完的时间限制,而是指两次数据发送中间的间隔超过connectionTimeout会被服务器断开。可以模拟一下,先修改server.xml,把connectionTimeout设置为2秒:

<Connector port="7001"
    protocol="HTTP/1.1"
    connectionTimeout="2000"
    redirectPort="8443" />

先看看是否已生效:

$ time telnet localhost 7001
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Connection closed by foreign host.
telnet localhost 7001  0.01s user 0.00s system 0% cpu 2.016 total

telnte后没有发送数据,看到2秒左右被服务器关闭了,证明配置生效了。

现在通过telnet发送数据:

$ telnet localhost 7001
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
POST /main HTTP/1.1
host: localhost:7001
Content-type:application/x-www-form-urlencoded
Content-length:10

a

上面我们模拟一次POST请求,指定的长度是10,但指发送了一个字符,这里等待2秒,会被服务器端认为超时,被强制关闭。response信息如下:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Length: 10
Date: Thu, 04 Sep 2014 08:20:08 GMT

done: null
Connection closed by foreign host.

如果想对POST情况不使用connectionTimeout来限制,还有另外两个参数可用。这两个参数必须配合使用才行:

disableUploadTimeout="false"
connectionUploadTimeout="10000"

必须要设置disableUploadTimeoutfalse(默认是true),才可以对POST请求发送数据超时使用其他参数来设置,这样在发送数据的过程中最大可以等待的时间间隔就不再由connectionTimeout决定,而是由connectionUploadTimeout决定。

答疑:tomcat关闭脚本怎么确保不误杀其他进程

Q: tomcat的关闭过程是怎么触发的?是通过系统信号吗?如果存在多个tomcat进程,关闭时怎么保证不会误杀?

A: 这个过程可以跟踪一下关闭时的脚本就知道了。

$ bash -x ./catalina.sh stop
...
eval '"/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home/bin/java"'
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Dlog4j.defaultInitOverride=true
-Dorg.apache.tomcat.util.http.ServerCookie.ALLOW_EQUALS_IN_VALUE=true
-Dorg.apache.tomcat.util.http.ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0=true '
-Djava.endorsed.dirs="/data/server/tomcat/endorsed"'
-classpath '"/data/server/tomcat/bin/bootstrap.jar:/data/server/tomcat/bin/tomcat-juli.jar"' '
-Dcatalina.base="/data/server/tomcat"' '
-Dcatalina.home="/data/server/tomcat"' '
-Djava.io.tmpdir="/data/server/tomcat/temp"'
org.apache.catalina.startup.Bootstrap stop

可见是新启了一个java进程,调用org.apache.catalina.startup.Bootstrapmain方法,传入的stop参数。

跟踪一下这个新的java进程执行过程,堆栈大致如下:

at java.net.Socket.(Socket.java:208)
at org.apache.catalina.startup.Catalina.stopServer(Catalina.java:477)
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:601)
at org.apache.catalina.startup.Bootstrap.stopServer(Bootstrap.java:371)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:452)

Bootstrapmain方法里的,对stop参数会执行stopServer的操作:

...
else if (command.equals("stop")) {
    daemon.stopServer(args);
}

stopServer是通过反射调用的Catalina.stopServer,它通过解析当前CATALINA_HOME/conf/server.xml从中得到正在运行的tomcat实例的关闭端口(server port, 默认是8005)和关闭指令(默认是SHUTDOWN),然后通过socket连接到这个目标端口上,发送关闭指令。如果我们直接telnet到目标端口,然后输入指令也是一样的:

所以通过默认脚本关闭tomcat,并不关心tomcat进程pid,而是socket通讯的方式。如果存在多个tomcat实例,每个tomcat的server port都是不同的。

如果不通过8005端口的方式,而是系统信号的方式,tomcat则是通过了ShutdownHook来确保在进程退出前关闭服务的。这时如果有多个tomcat进程实例,就需要明确进程pid了,一些改进的脚本会在启动时把进程pid记录在某个文件来以便后续使用。