tomcat8.5.8遇到的两个问题

压力测试场景,前端nginx反向代理到4个tomcat实例,在其中的一个实例上产生了大量的countDownConnection Incorrect connection count警告

 WARNING [http-nio-8080-exec-48] org.apache.tomcat.util.net.AbstractEndpoint.countDownConnection Incorrect connection count, multiple socket.close called on the same socket.

另外一个异常是4个tomcat实例上都看到的NPE异常:

Exception in thread "http-nio-8080-AsyncTimeout" java.lang.NullPointerException
  at org.apache.coyote.AbstractProcessor.doTimeoutAsync(AbstractProcessor.java:528)
  at org.apache.coyote.AbstractProcessor.timeoutAsync(AbstractProcessor.java:518)
  at org.apache.coyote.AbstractProtocol$AsyncTimeout.run(AbstractProtocol.java:1130)
  at java.lang.Thread.run(Thread.java:745)

使用了servlet3.0,connector配置如下:

 <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
           connectionTimeout="20000" redirectPort="8443" maxParameterCount="2000" maxKeepAliveRequests="-1"
       maxThreads="200" maxPostSize="20971520" acceptCount="1024" useBodyEncodingForURI="true"
        URIEncoding="UTF-8"/>

已将bug提交到了bugzilla,在这里记录一下,后续跟踪。

scala雾中风景(27): lambda表达式里的return是抛异常实现的

尽管Scala和其他函数式编程都不太鼓励使用return,但我不喜欢太多缩进的代码,所以很少用较多的逻辑嵌套(除非是那种非常对称的嵌套),而是喜欢将不满足条件的先return掉。最近遇到一个scala里的流控陷阱,即在lambda里的return背后实际是通过特定的异常来实现的。

对于流控和异常捕获之前也遇到过其他陷阱,但稍不留意仍可能重犯,比如下面这段代码:

try {
    ...
    for (topicMeta <- resp.topicsMetadata; partMeta <- topicMeta.partitionsMetadata) {
      if (topicMeta.topic == p.topic && partMeta.partitionId == p.partition) {
        redisClient.hset(key, field, node.host + ":" + node.port)
        return
      }
    }
} catch {
    case e: Throwable =>
    ...
}

它是一段在actor里的逻辑,因为不希望非预期异常导致这个actor重启,所以是对Throwable进行的捕获,然而运行时竟捕获到了scala.runtime.NonLocalReturnControl$mcV$sp这样的异常。for语法糖容易让人忘记它里面的操作可能是一段匿名函数,简化一下这个例子:

➜  cat Test.scala
object Test {
    def main(args: Array[String]) {
        val list = List("A", "B", "C")
        for (e1 <- list) {
            if (e1 == "C") {
                println("ok, do something.")
                return
            }
        }
    }   
}

看看它编译的中间环节:

➜  scalac -Xprint:explicitouter Test.scala
[[syntax trees at end of             explicitouter]] // Test.scala
package <empty> {
object Test extends Object {
def <init>(): Test.type = {
  Test.super.<init>();
  ()
};
def main(args: Array[String]): Unit = {
  <synthetic> val nonLocalReturnKey1: Object = new Object();
  try {
    val list: List[String] = immutable.this.List.apply[String](scala.this.Predef.wrapRefArray[String](Array[String]{"A", "B", "C"}));
    list.foreach[Unit]({
      @SerialVersionUID(value = 0) final <synthetic> class $anonfun extends scala.runtime.AbstractFunction1[String,Unit] with Serializable {
        def <init>(): <$anon: String => Unit> = {
          $anonfun.super.<init>();
          ()
        };
        final def apply(e1: String): Unit = if (e1.==("C"))
          {
            scala.this.Predef.println("ok, do something.");
            throw new scala.runtime.NonLocalReturnControl$mcV$sp(nonLocalReturnKey1, ())
          }
        else
          ()
      };
      (new <$anon: String => Unit>(): String => Unit)
    })
  } catch {
    case (ex @ (_: scala.runtime.NonLocalReturnControl[Unit @unchecked])) => if (ex.key().eq(nonLocalReturnKey1))
      ex.value$mcV$sp()
    else
      throw ex
  }
}
}
}

很明显return在嵌套的匿名函数里是无法跳出外层函数的,所以编译器通过抛出 scala.runtime.NonLocalReturnControl 异常来实现跳出最外层。所有的lambda中使用return都是如此:

def main(args: Array[String]) {
    val lambda: String => Unit =
        (str: String) => { if ("hit" == str) return } //跳出main

    try {
        lambda("hit")
    } catch {
        case e: Throwable => //scala.runtime.NonLocalReturnControl
        e.printStackTrace()
    }
}

还是要注意对Throwable的捕获,不要干扰流控逻辑:

try{
    ...
}catch {
case e0: ControlThrowable => throw e0 // 不要干预流控的异常
case e1: Throwable => e1.printStackTrace
}   

JDK在mac和linux上对connection reset的行为不一致?

遇到一个JDK(版本都是1.8.0_51-b16)在mac和linux上行为不一致的问题,这个问题是针对redis服务器端关闭连接时的状况处理;即在redis-server端设置连接的空闲时间,当超过这个空闲时间后server主动把该连接关闭掉。在我的mac上这个行为是符合预期的,对这个超时的连接再发起请求时会抛出 SocketException "Connection Reset",但在linux上却不会。

简单模拟一下,在Redis服务端配置timeout为3秒,客户端建立连接之后 sleep 4秒(或更久),让连接超时

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;

public class ConnRestTest {

    public static void main(String[] args) throws Exception {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("localhost", 6379));

        InputStream is = socket.getInputStream();
        OutputStream os = socket.getOutputStream();

        Thread.sleep(4000);

        os.write("*1\r\n$4\r\nPING\r\n".getBytes());
        os.flush();

        for (int i = 0; i < 7; i++) {
            int r = is.read();
            System.out.print( r == -1 ? r : (char) r);
        }

        socket.close();
    }
}

上面的代码在mac上运行后会看到如下异常(如果注释掉 Thread.sleep(4000) 会得到 "+PONG\r\n")

Exception in thread "main" java.net.SocketException: Connection reset
    at java.net.SocketInputStream.read(SocketInputStream.java:209)
    at java.net.SocketInputStream.read(SocketInputStream.java:141)
    at java.net.SocketInputStream.read(SocketInputStream.java:223)
    at com.wacai.common.redis.io.Test.main(Test.java:23)    

而在linux上运行后则是打印7个连续的"-1",即并不会抛出"Connection reset"

分别进行tcpdump,在mac上:

 ➜  sudo tcpdump -i lo0 port 6379

23:13:43.398003 IP localhost.63864 > localhost.6379: Flags [S], seq 1658673677, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1063188198 ecr 0,sackOK,eol], length 0
23:13:43.398064 IP localhost.6379 > localhost.63864: Flags [S.], seq 355134851, ack 1658673678, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1063188198 ecr 1063188198,sackOK,eol], length 0
23:13:43.398075 IP localhost.63864 > localhost.6379: Flags [.], ack 1, win 12759, options [nop,nop,TS val 1063188198 ecr 1063188198], length 0
23:13:43.398085 IP localhost.6379 > localhost.63864: Flags [.], ack 1, win 12759, options [nop,nop,TS val 1063188198 ecr 1063188198], length 0

23:13:47.063640 IP localhost.6379 > localhost.63864: Flags [F.], seq 1, ack 1, win 12759, options [nop,nop,TS val 1063191852 ecr 1063188198], length 0
23:13:47.063671 IP localhost.63864 > localhost.6379: Flags [.], ack 2, win 12759, options [nop,nop,TS val 1063191852 ecr 1063191852], length 0

23:13:48.403144 IP localhost.63864 > localhost.6379: Flags [P.], seq 1:15, ack 2, win 12759, options [nop,nop,TS val 1063193184 ecr 1063191852], length 14
23:13:48.403255 IP localhost.6379 > localhost.63864: Flags [R], seq 355134853, win 0, length 0  

在linux上:

$ sudo tcpflow -p -c -i lo port 6379

00:26:13.303233 IP localhost.34609 > localhost.6379: Flags [S], seq 1094106697, win 43690, options [mss 65495,sackOK,TS val 8462190 ecr 0,nop,wscale 7], length 0
00:26:13.303272 IP localhost.6379 > localhost.34609: Flags [S.], seq 2755621045, ack 1094106698, win 43690, options [mss 65495,sackOK,TS val 8462190 ecr 8462190,nop,wscale 7], length 0
00:26:13.303298 IP localhost.34609 > localhost.6379: Flags [.], ack 1, win 342, options [nop,nop,TS val 8462190 ecr 8462190], length 0

00:26:17.037992 IP localhost.6379 > localhost.34609: Flags [F.], seq 1, ack 1, win 342, options [nop,nop,TS val 8465925 ecr 8462190], length 0
00:26:17.038450 IP localhost.34609 > localhost.6379: Flags [.], ack 2, win 342, options [nop,nop,TS val 8465926 ecr 8465925], length 0

00:26:18.305591 IP localhost.34609 > localhost.6379: Flags [P.], seq 1:15, ack 2, win 342, options [nop,nop,TS val 8467193 ecr 8465925], length 14
00:26:18.305630 IP localhost.6379 > localhost.34609: Flags [R], seq 2755621047, win 0, length 0     

在mac上比linux上多了一次在连接建立后从server发给client端的ack,在3秒之后,连接因为超过空闲时间server端向client端发起了fin,client回复ack,之后client端程序对socket进行写操作,在tcpdump里看到标记为P的标记,然后server端发送了reset

在linux上看到连接状态的变化也符合预期:

$ netstat -antp | grep 6379
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:6379          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:6379          127.0.0.1:34607         ESTABLISHED -
tcp6       0      0 127.0.0.1:34607         127.0.0.1:6379          ESTABLISHED 3683/java

tcp        0      0 127.0.0.1:6379          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:6379          127.0.0.1:34607         FIN_WAIT2   -
tcp6       1      0 127.0.0.1:34607         127.0.0.1:6379          CLOSE_WAIT  3683/java

按说server端发送reset标记后,mac上抛出异常的行为更符合预期,可能是JDK网络层面的实现细节不同,或者tcpdump出的数据已经能解释原因只是我看不出来。

zsh的字符串替换引起的卡顿

我的mac系统每次启动后第一次打开iterm2的时候,oh-my-zsh的启动总是明显的卡顿一下,而之后退出iterm2重启动则不会有这个卡顿,也就是只在第一次启动iterm2的时候发生。对启动的zsh增加了-xv参数后观察,发现这个卡顿发生在git_compare_version函数的第4行:

找到这个函数后,发现第4行的操作并不是git等网络操作,而是一个字符串替换的操作,它使用zsh内置的字符串替换功能:INSTALLED_GIT_VERSION=(${(s/./)INSTALLED_GIT_VERSION[3]})

非常的不符合直觉(直觉上以为卡顿是因为网络阻塞引起的),模拟一下,在一个脚本里使用这个字符串替换操作,看看具体的耗时情况:

$ cat zsh-test.sh
#!/usr/bin/env zsh -xv
export PS4=$'%D{%M%S%.} %N:%i> '

INSTALLED_GIT_VERSION=($(command git --version 2>/dev/null));
INSTALLED_GIT_VERSION=(${(s/./)INSTALLED_GIT_VERSION[3]});
echo "$INSTALLED_GIT_VERSION"

然后再启动时调用这个zsh脚本:

$ cat run.sh
#!/usr/bin/env zsh -xv
export PS4=$'%D{%M%S%.} %N:%i> '
./zsh-test.sh

重启系统,启动后在bash下执行run.sh脚本:

INSTALLED_GIT_VERSION=($(command git --version 2>/dev/null));
5649865 ./zsh-test.sh:4> INSTALLED_GIT_VERSION=5649867 ./zsh-test.sh:4> git --version
5649865 ./zsh-test.sh:4> INSTALLED_GIT_VERSION=( git version 2.8.4 '(Apple' 'Git-73)' ) 
INSTALLED_GIT_VERSION=(${(s/./)INSTALLED_GIT_VERSION[3]});
5650999 ./zsh-test.sh:5> INSTALLED_GIT_VERSION=( 2 8 4 )

看到zsh-test.sh里的第5行字符串替换的操作耗时用了1秒多时间,如果再次执行的话会降到几个毫秒。这真是个蹊跷的问题,发邮件给 zsh-works@zsh.org 好几周也没有人回复,先在博客里记录一下这个问题,以后再追踪。zsh版本是:5.2 (x86_64-apple-darwin16.0.0)。

作业控制与前台进程组

这篇文章是对之前的SIGTTIN信号量的疑惑?的解答,对于为何会有这种奇怪的用法,在另一篇shell下精确的定位一个命令 也介绍过了,这里想讨论的重点不在于怎么变通解决那个问题,而是导致SIGTTIN发生的机制是怎么引起的。我的同事对这个问题也产生了好奇,在stackoverflow上发帖,有人给出了解释,解答的人直接给出了bash的源码jobs.c里的initialize_job_control方法片段,指出SIGTTIN正是那里面的逻辑。不过如果你跟我一样对shell和linux系统调用都懂得很肤浅的话,这段代码并不容易懂,所以在这里更详细的解释一下这个问题的来龙去脉。

刚开始碰到这个问题的时候,通过strace看到了是SIGTTIN信号量所致,因为这个信号量默认的行为是让进程STOP(暂停),即通过ps观察到的状态为T。对于SIGTTIN信号量《Linux/UNIX系统编程手册》上是这么说的:

只有前台作业中的进程才能够从控制终端读取输入。这个限制条件避免了多个作业竞争读取终端输入。如果后台作业尝试从终端读取输入,就会接收到一个SIGTTIN信号。SIGTTIN信号的默认处理动作是停止作业。

但我们的脚本里并没有后台进程,那两个进程也没有读取终端,跟上面的解释对不上。也没有在网上搜到其它引发SIGTTIN信号的情况,在这里困惑了很久。不过凭直觉知道这个问题应该跟作业控制有关,在脚本里显式的开启作业控制,是能够正常运行的:

$ cat sleep.sh
#!/bin/bash
set -m
bash -ic 'sleep 3'
bash -ic 'sleep 2'

所以一定是在进程某个状态上的不一致导致的。上周末的时候阅读了一下strace的log,对出问题的脚本:

#!/bin/bash
bash -ic 'sleep 3'
bash -ic 'sleep 2'

使用strace -f -e verbose=all -t ./sleep.sh 2>log 得到更详细的日志

...
03:39:06 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f997f03ca10) = 9897
...
[pid  9897] 03:39:06 execve("/usr/bin/bash", ["bash", "-ic", "sleep 3"], [/* 30 vars */]) = 0
...
[pid  9897] 03:39:06 open("/dev/tty", O_RDWR|O_NONBLOCK) = 3
[pid  9897] 03:39:06 getrlimit(RLIMIT_NOFILE, {rlim_cur=1024, rlim_max=4*1024}) = 0
[pid  9897] 03:39:06 fcntl(255, F_GETFD) = -1 EBADF (Bad file descriptor)
[pid  9897] 03:39:06 dup2(3, 255)       = 255
[pid  9897] 03:39:06 close(3)           = 0
[pid  9897] 03:39:06 ioctl(255, TIOCGPGRP, [9891]) = 0
[pid  9897] 03:39:06 setpgid(0, 9897)   = 0 //第一个子进程更改了它的进程组ID
...
03:39:09 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f997f03ca10) = 9922
...
[pid  9922] 03:39:09 execve("/usr/bin/bash", ["bash", "-ic", "sleep 2"], [/* 30 vars */]) = 0
...
[pid  9922] 03:39:09 access("/usr/bin/bash", R_OK) = 0
[pid  9922] 03:39:09 getpgrp()          = 9891
[pid  9922] 03:39:09 ioctl(2, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffd356e49c0) = -1 ENOTTY (Inappropriate ioctl for device)
[pid  9922] 03:39:09 open("/dev/tty", O_RDWR|O_NONBLOCK) = 3
[pid  9922] 03:39:09 getrlimit(RLIMIT_NOFILE, {rlim_cur=1024, rlim_max=4*1024}) = 0
[pid  9922] 03:39:09 fcntl(255, F_GETFD) = -1 EBADF (Bad file descriptor)
[pid  9922] 03:39:09 dup2(3, 255)       = 255
[pid  9922] 03:39:09 close(3)           = 0
[pid  9922] 03:39:09 ioctl(255, TIOCGPGRP, [9897]) = 0
[pid  9922] 03:39:09 rt_sigaction(SIGTTIN, {SIG_DFL, [], SA_RESTORER, 0x7f912a22b650}, {SIG_IGN, [], SA_RESTORER, 0x7f912a22b650}, 8) = 0
[pid  9922] 03:39:09 kill(0, SIGTTIN)   = 0
[pid  9896] 03:39:09 <... wait4 resumed> 0x7ffc5b5e6800, 0, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
[pid  9922] 03:39:09 --- SIGTTIN {si_signo=SIGTTIN, si_code=SI_USER, si_pid=9922, si_uid=1000} ---
[pid  9896] 03:39:09 --- SIGTTIN {si_signo=SIGTTIN, si_code=SI_USER, si_pid=9922, si_uid=1000} ---
[pid  9922] 03:39:09 --- stopped by SIGTTIN ---
[pid  9896] 03:39:09 --- stopped by SIGTTIN ---

确认这个SIGTTIN信号是第二个bash -ic 'sleep 2'进程发出的,kill(0, SIGTTIN)表示它把这个信号发送到自己所在的进程组,整个进程组的进程都接收到这个信号,所以它和它的父进程sleep.sh都变成了stop状态。

脚本里两次执行的bash -ic子进程也都是shell,它们在初始化的时候会有作业控制的逻辑,结合jobs.cinitialize_job_control方法里的代码(shell初始化时调用到这里):

/* We can only have job control if we are interactive. */
if (interactive == 0)
{
  job_control = 0;
  original_pgrp = NO_PID;
  shell_tty = fileno (stderr);
}
else
{
  shell_tty = -1;

  /* If forced_interactive is set, we skip the normal check that stderr
    is attached to a tty, so we need to check here.  If it's not, we
    need to see whether we have a controlling tty by opening /dev/tty,
    since trying to use job control tty pgrp manipulations on a non-tty
    is going to fail. */ 
    // bash "-i" 参数会启用 forced_interactive
  if (forced_interactive && isatty (fileno (stderr)) == 0)
    shell_tty = open ("/dev/tty", O_RDWR|O_NONBLOCK);

  /* Get our controlling terminal.  If job_control is set, or
    interactive is set, then this is an interactive shell no
     matter where fd 2 is directed. */
   if (shell_tty == -1)
    shell_tty = dup (fileno (stderr));/* fd 2 */

  shell_tty = move_to_high_fd (shell_tty, 1, -1);

  /* Compensate for a bug in systems that compiled the BSD
 rlogind with DEBUG defined, like NeXT and Alliant. */
  if (shell_pgrp == 0)
{
  shell_pgrp = getpid ();
  setpgid (0, shell_pgrp);
  tcsetpgrp (shell_tty, shell_pgrp);
}

  while ((terminal_pgrp = tcgetpgrp (shell_tty)) != -1)
{
  if (shell_pgrp != terminal_pgrp)
    {
      SigHandler *ottin;

      ottin = set_signal_handler(SIGTTIN, SIG_DFL);
      kill (0, SIGTTIN); // 第二次执行bash -ic时触发了这里
      set_signal_handler (SIGTTIN, ottin);
      continue;
    }
  break;
}
if (terminal_pgrp == -1)
t_errno = errno;

  /* Make sure that we are using the new line discipline. */
  if (set_new_line_discipline (shell_tty) < 0)
{
  sys_error (_("initialize_job_control: line discipline"));
  job_control = 0;
}
  else
{
  original_pgrp = shell_pgrp;
  shell_pgrp = getpid ();

  // 第一次bash -ic 'sleep 3'触发了这里的 setpgid 修改了当前进程组
  if ((original_pgrp != shell_pgrp) && (setpgid (0, shell_pgrp) < 0))
    {
      sys_error (_("initialize_job_control: setpgid"));
      shell_pgrp = original_pgrp;
    }

  job_control = 1;

  /* If (and only if) we just set our process group to our pid,
     thereby becoming a process group leader, and the terminal
     is not in the same process group as our (new) process group,
     then set the terminal's process group to our (new) process
     group.  If that fails, set our process group back to what it
     was originally (so we can still read from the terminal) and
     turn off job control.  */
  if (shell_pgrp != original_pgrp && shell_pgrp != terminal_pgrp)
    {
      if (give_terminal_to (shell_pgrp, 0) < 0)
    {
      t_errno = errno;
      setpgid (0, original_pgrp);
      shell_pgrp = original_pgrp;
      job_control = 0;
    }
   }
...

关键点就在于shell_pgrpterminal_pgrp这两个变量,shell_pgrp是当前进程组,而terminal_pgrp是占用当前控制终端的进程所在的进程组(前台进程组),这些状态都是可以通过ps观察到的,可以跟踪一下:

$ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/2/"
  PID  PPID  PGID   SID TPGID STAT TT       COMMAND
12413 12410 12410 12410    -1 S    ?        sshd: hongjiang@pts/0
12414 12413 12414 12414 12580 Ss   pts/0     \_ -bash
12579 12414 12579 12414 12580 S    pts/0         \_ /bin/bash ./sleep.sh
12580 12579 12580 12414 12580 S+   pts/0             \_ sleep 3

$ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/2/"
  PID  PPID  PGID   SID TPGID STAT TT       COMMAND
12413 12410 12410 12410    -1 S    ?        sshd: hongjiang@pts/0
12414 12413 12414 12414 12414 Ss+  pts/0     \_ -bash
12579 12414 12579 12414 12414 T    pts/0         \_ /bin/bash ./sleep.sh
12607 12579 12579 12414 12414 T    pts/0             \_ bash -ic sleep 2

在第一次执行bash -ic 'sleep 3'的时候,sleep.sh父进程先clone出bash子进程(pid 12580),因为-i参数强制这个bash子进程用交互式运行,它会加载$HOME下的.bashrc等文件,这个过程可能会fork/clone出若干子进程(所以会看到第二次bash -ic sleep 2进程的ID跟第一次不是连续的),等这些配置文件加载完之后,它并不是fork/clone的形式执行sleep 3而是使用当前进程(12580)执行的sleep 3,这里很关键的信息是"PGID"和"TPGID"都是本身进程ID,而非父进程ID,跟第二次的状态不一样。

因为脚本默认是关闭作业控制的,本来每个子进程并不会设置为独立的进程组,比如下面这个脚本:

$ cat a.sh
#!/bin/bash
/usr/bin/sleep 10

$ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/2/"
  PID  PPID  PGID   SID TPGID STAT TT       COMMAND
12668 12665 12665 12665    -1 S    ?        sshd: hongjiang@pts/2
12669 12668 12669 12669 12736 Ss   pts/2     \_ -bash
12736 12669 12736 12669 12736 S+   pts/2         \_ /bin/bash ./a.sh
12737 12736 12736 12669 12736 S+   pts/2             \_ /usr/bin/sleep 10

上面脚本执行时sleep子进程"PGID"和"TPGID"都是进程父进程a.sh的,并没有被设置为一个独立的进程组。

sleep.sh脚本里之所以会对子进程设置一个独立的进程组,是因为"-i"参数使得bash -ic 'sleep 3'在非交互式脚本里运行时进程被强制设置成了独立的进程组(见initializejobcontroll里的setpgid),同时"TPGID"这个表示前台进程组的状态也被改为了bash -ic 'sleep 3'的进程组ID。

那为什么在接下来的bash -ic 'sleep 2'子进程执行时却不像前面的那样呢?这正是最诡异的地方。它们所在的sleep.sh脚本是非交互式运行的,它本来预期脚本执行过程不应该产生与脚本进程组不一致的前台进程组,所以前台子进程组结束的时候,不会去更新"TPGID",可以用下面脚本来验证:

$ cat wait.sh
#!/bin/bash
bash -ic 'sleep 5'
sleep 4
sleep 3 &  #不让wait.sh进程立即退出
wait

上面wait.sh脚本里第一个子进程强制修改了"TPGID",子进程退出,以及后续再执行前台进程都不会去更新这个状态

$ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/0/"
  PID  PPID  PGID   SID TPGID STAT TT       COMMAND
13867 13864 13864 13864    -1 S    ?        sshd: hongjiang@pts/0
13868 13867 13868 13868 14138 Ss   pts/0     \_ -bash
14137 13868 14137 13868 14138 S    pts/0         \_ /bin/bash ./wait.sh
14138 14137 14138 13868 14138 S+   pts/0             \_ sleep 5

$ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/0/"
  PID  PPID  PGID   SID TPGID STAT TT       COMMAND
13867 13864 13864 13864    -1 S    ?        sshd: hongjiang@pts/0
13868 13867 13868 13868 14138 Ss   pts/0     \_ -bash
14137 13868 14137 13868 14138 S    pts/0         \_ /bin/bash ./wait.sh
14165 14137 14137 13868 14138 S    pts/0             \_ sleep 4

$ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/0/"
  PID  PPID  PGID   SID TPGID STAT TT       COMMAND
13867 13864 13864 13864    -1 S    ?        sshd: hongjiang@pts/0
13868 13867 13868 13868 14138 Ss   pts/0     \_ -bash
14137 13868 14137 13868 14138 S    pts/0         \_ /bin/bash ./wait.sh
14168 14137 14137 13868 14138 S    pts/0             \_ sleep 3

回到sleep.sh脚本里,第二行bash -ic 'sleep 2'子进程初始化时,"TPGID"仍是上个进程bash -ic 'sleep 3'修改过的值。而bash -ic 'sleep 2'子进程也因为"-i"参数让自己以交互式运行,但是在还没有执行到setpgid之前,就先触发了SIGTTIN的逻辑:

if (shell_pgrp != terminal_pgrp)
{
    SigHandler *ottin;

    ottin = set_signal_handler(SIGTTIN, SIG_DFL);
    kill (0, SIGTTIN);
    set_signal_handler (SIGTTIN, ottin);
    continue;
}

因为这段代码会认为终端被其他前台进程占用,对当前进程组发出SIGTTIN信号。在这个场景里,这恰好是一种误会!

当我们显式的对sleep.sh脚本设置开启作业控制时:

$ cat sleep.sh
#!/bin/bash
set -m
bash -ic 'sleep 3'
bash -ic 'sleep 2'

$ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/2/"
  PID  PPID  PGID   SID TPGID STAT TT       COMMAND
12668 12665 12665 12665    -1 S    ?        sshd: hongjiang@pts/2
12669 12668 12669 12669 12874 Ss   pts/2     \_ -bash
12873 12669 12873 12669 12874 S    pts/2         \_ /bin/bash ./sleep.sh
12874 12873 12874 12669 12874 S+   pts/2             \_ sleep 3

$ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/2/"
  PID  PPID  PGID   SID TPGID STAT TT       COMMAND
12668 12665 12665 12665    -1 S    ?        sshd: hongjiang@pts/2
12669 12668 12669 12669 12901 Ss   pts/2     \_ -bash
12873 12669 12873 12669 12901 S    pts/2         \_ /bin/bash ./sleep.sh
12901 12873 12901 12669 12901 S+   pts/2             \_ sleep 2

它对每个子进程都设置为独立的进程组,并在每个进程(前台)结束的时候更新"TPGID"为父进程组ID,避免了initialize_job_controll里发送SIGTTIN的逻辑。

有很多shell的问题都是跟作业控制相关的,另一个例子参考tomcat进程意外退出的问题分析;作业控制可以玩出很多高阶花样,但它也大大增加了shell的复杂度,这个例子是一个典型的反面教材,最好不要在非交互式脚本里调用bash -ic来执行命令。

Redis返回(nil)在lua里是boolean类型而非nil

Redis里执行gethget不存在的key或field时返回值在终端显式的是”(nil)”

127.0.0.1:6379> get notexist
(nil)

当使用lua脚本执行逻辑时,如果要判断这个值,很容易让人迷惑以为它是nil,从而导致判断不成立,实际它是一个boolean的值

127.0.0.1:6379> eval "local v=redis.call('get',KEYS[1]); return type(v)" 1 notexist
"boolean"

所以在脚本里判断获取的结果不为空,正确的方式还有判断false的情况:

local v=redis.call('hget',KEYS[1], ARGV[1]); 
if (v ~= nil or (type(v) == 'boolean' and v)) then 
    return 'not-empty'; 
end

Sierra三宗罪

1) Sierra下很多原有软件无法运行

有些通过mac store下载的需要重新下载才可运行:

搜狗拼音无法使用,需要下载最新版本重新安装。

2) 有个photoanalysisd的进程在后台分析你的图片,导致cpu占用率很高,可以用下面方式停止:

 ➜  cat stop-photoanalysisd.sh
#!/bin/bash

launchctl stop com.apple.photoanalysisd
launchctl unload -w /System/Library/LaunchAgents/com.apple.photoanalysisd.plist

3) 原先VPN配置里的PPTP模式都丢失了

《Scala函数式编程》中文版勘误

感谢林晴、史珩、shahito、乐乐Joker 以及其他匿名用户的反馈,勘误表如下(另外,原书作者另写了一本伴生书,对文章的错别字以及个别代码样例错误做了纠正,伴生书pdf可从作者网站免费下载:http://blog.higher-order.com/assets/fpiscompanion.pdf 为了保持一致,伴生书中修订的内容在中文版中并没有修改。伴生书中指出的错误并不多,也不影响本书阅读,主要是为书中的练习做解释)

页数:目录
 原文:1.3 引用透明、纯粹度以及替代模型
 修改:1.3 引用透明、纯粹性以及代换模型

页数:目录 XI
 原文:4 不是用异常来处理错误
 修改:4 不使用异常来处理错误

页数:目录 XII
 原文:第二部分  功能设计和组合子库
 修改:第二部分  函数式设计和组合子库

 原文:9.1 代数设计,走起
 修改:9.1 代数设计

页数:目录 XIII
 原文:第三部分  函数设计的通用结构
 修改:第三部分  函数式设计的通用结构

页数:目录 XV
 原文:14 本地影响和可变状态
 修改:14 局部作用和可变状态

 原文:14.3 纯粹是相对于上下文的
 修改:14.3 纯粹性是相对于上下文的

页数:原推荐序
 第一段中
 原文:函数式编程作为书题出现在Scala中是个有趣的现象。
 修改:《Scala函数式编程》是个有趣的书名。

 第二段中
 原文:它同时承认非纯粹函数和纯函数
 修改:它同时允许非纯粹函数和纯函数

 第五段中
 原文:从第一个原理扩展到
 修改:从首要原则扩展到

页数:第一部分 函数式编程介绍
 第一段中
 原文:我们以一个激进的前提开始读这本书
 修改:我们以一种激进的前提开始这本书

 原文:比如读取文件或修改内存时。
 修改:比如读取文件或修改内存。

 第二段中
 原文:并给你一些有益的理念。
 修改:让你对函数式编程的好处有些概念。

页数:8
 原文:引用透明与纯粹度
 修改:引用透明与纯粹性

 原文:1.3 引用透明、纯粹度以及替代模型
 修改:1.3 引用透明、纯粹性以及代换模型

 // 统一翻译为“代换模型”
 原文:我们称之为代替模型( substitution model )。
 修改:我们称之为代换模型( substitution model )。

 原文:其中一个例子的所有表达式都是引用透明的,可用替代模型来推导,
 修改:其中一个例子的所有表达式都是引用透明的,可用代换模型来推导,

 原文:让我们尝试在Scala解析器(也称作REPL,
 修改:让我们尝试在Scala解释器(也称作REPL,

页数:10
 原文:与之相反的是,替换模型则很容易推理
 修改:与之相反的是,代换模型则很容易推理

 原文:即使没有用过“替代模型”这一名词,
 修改:即使没有用过“代换模型”这一名词,

 原文:还对引用透明和代替模型进行了讨论,
 修改:还对引用透明和代换模型进行了讨论,

页数:22
 // andThan -> andThen
 原文:同时还提供了一个 andThan 方法, f andThan g 等价于 g compose f
 修改:同时还提供了一个 andThen 方法, f andThen g 等价于 g compose f

页数:23
 原文:对于像这样小的一行层序,还不算困难
 修改:对于像这样的一行小程序,还不算困难

页数:40
 原文:不是用异常来处理错误
 修改:不使用异常来处理错误

页数:41
 原文:让替代模型的简单推导无法适用
 修改:让代换模型的简单推导无法适用

页数:54
  原文(最下面的注释): 我们现在使用Scala标注库
  修改:我们现在使用Scala标准库

页数:57
 原文:如果表达式 f(x) 对所有的evaluates to bottom的表达是x,也是evaluates to bottom,那么f是严格求值的。
 修改:如果表达式f(x)对于所有evaluates to bottom的表达式x同样是evaluates to bottom的,那么f是严格求值的。

页数:67
 原文:目的是模拟投6面色子死亡法
 修改:目的是模拟投6面体骰子

页数:73
 原文:这是向右移的方式,
 修改:这是朝着正确方向前进,

 原文:用纯函数式API实现一个更加可测的死亡色子?
 修改:用纯函数式API实现一个更加可测的投骰子?

 原文:这里是一个死亡色子 rollDie 的实现,
 修改:这里是一个投骰子 rollDie 的实现,

页数:77
 原文:第二部分 功能设计和组合子库
 修改:第二部分 函数式设计和组合子库

页数:81
 原文:两边的表达式是无法实现平行执行的。
 修改:两边的表达式是无法实现并行执行的。

 注释2
 原文:并在衍生下一个并行计算之前等待前一个并行计算完成,这样的计算比较高效同时也保证串行执行。
 修改:并在衍生下一个并行计算之前等待前一个并行计算完成,这样的计算实际是串行的。

页数:90
 原文:(法则常常是从标识的具体例子中得出来的)
 修改:(法则常常是从恒等式的具体例子中得出来的)

 注释8
 原文:这里的标识我们想表达的是,在数学上指两个表达式相同或等价
 修改:这里的恒等式表达的是,在数学上指两个表达式相同或等价

页数:91
 //将 fascinating 翻译为了“醉了吧” 与整本书的语言风格不一致,删除。
 原文:醉了吧!从最后的法则可以看出对于map而言unit明显是个多余的细节。
 修改:从最后的法则可以看出对于map而言unit明显是个多余的细节。

 原文:由于我们得到的这个第二法则或定理是自由的,仅仅是因为map的参数态(parametricity),它有时也被称为自由定理(free theorem)。
 修改:由于我们得到的这个第二法则或定理是免费的,仅仅是因为map的参数态(parametricity),它有时也被称为免费定理(free theorem)。

 注释10
 原文:这和我们在代数方程式里做的简化过程一样。
 修改:这和我们在代数方程式里做的代换和简化过程一样。

 注释12 // 译注:在优化界有一个著名的定理叫“没有免费午餐定理”,这个论文的题目可能是想与之呼应
 原文:”自由定理”的观点来自于 Philip Wadler 的经典论文《Theorem for Free》(http://mng.bz/Z9f1)
 修改:”免费定理”的观点来自于 Philip Wadler 的经典论文《Theorem for Free》(http://mng.bz/Z9f1)

页数:103
 原文:某些情况下,Gen[A] 的空间(domain)足够小
 修改:某些情况下,Gen[A] 的定义域(domain)足够小

 注释2
 原文:这里的“空间”(domian)与函数空间一样
 修改:这里的“定义域”(domian)与函数定义域一样

页数:108
 // Refining 在编译器里更多翻译为”具化"
 原文:8.2.5 精炼 Prop 的数据类型
 修改:8.2.5 具化 Prop 的数据类型

页数:117
 注释13
 原文:回忆一下在第7章中我们曾介绍过的自由定理,
 修改:回忆一下在第7章中我们曾介绍过的免费定理,

页数:117
 // 风格不符
 原文:9.1 代数设计,走起
 修改:9.1 代数设计

页数:153
 注释3
 原文:因为借用了已经存在的数据抽象名称
 修改:因为借用了已经存在的数学抽象名称

页数:164
 // primitive翻译为原语,保持一致风格
 原文:State中(除了unit和flatMap)其他原始的操作
 修改:State中(除了unit和flatMap)其他原语操作

 原文:它们和monadic原始操作(unit和flatMap) 一起构成了State数据类型的所有操作。monad一般都是这样的,它们都包括unit和flatMap,并且每个monad又有自己额外的原始操作。
 修改:它们和monadic原语操作(unit和flatMap) 一起构成了State数据类型的所有操作。monad一般都是这样的,它们都包括unit和flatMap,并且每个monad又有自己额外的原语操作。

页数:172
 原文:Applicative构建了上下文自由的计算,
 修改:Applicative构建了上下文无关的计算,

页数:210
 原文:本地影响和可变状态
 修改:局部作用和可变状态

页数:212
 注释1
 原文:也无须每次在充分利用本地变更(local mutation)时使用。
 修改:也无须每次在充分利用局部变更(local mutation)时使用。

页数:214
 // primitive 统一翻译为原语
 原文:这里依旧采用组合子库加一些基元函数(primitive)的形式,其中关于可变内存单元的应有的基元函数有:
 修改:这里依旧采用组合子库加一些原语函数(primitive)的形式,其中关于可变内存单元的应有的原语函数有:

页数:217
 // primitive 统一翻译为原语
 原文:为此,我们需要先实现可变数组的基元组合子:
 修改:为此,我们需要先实现可变数组的原语组合子:

页数:218
 // 基元->原语,负责->复杂
 原文:有了这些基元函数,我们便可以实现更负责的数组函数了。
 修改:有了这些原语函数,我们便可以实现更复杂的数组函数了。

 原文:我们不如把这变成一个基元函数:
 修改:我们不如把这变成一个原语函数:

页数:219
 练习14.3
 原文:为 scala.collection.mutable.HashMap 实现一组最小的基元组合子。
 修改:为 scala.collection.mutable.HashMap 实现一组最小的原语组合子。

页数:240
 原文:链状混合(Zipping)是Tee特有的一种情况,
 修改:拉链式操作(Zipping)是Tee特有的一种情况,

页数:242
 原文:请用存在基元函数实现join,
 修改:请用已存在的原语函数实现join,

优雅关闭与session draining

最近看到nginx plus收费版里有个session draining的概念,在用nginx做反向代理,要优雅停止后端应用的话,需要先在nginx层控制请求不再发到相应的节点,但与此同时已经建立的链接则要继续保持,一直等这些链接都结束了再停止应用。

其实在免费版的nginx/tengine就能实现这个特性,在以前的这篇nginx反向代理对后端某个节点优雅下线的问题 文章里测试过后端tomcat有一个耗时很长的请求,当tengine端健康监测已发现后端节点不可用的情况下,该请求并不会被tengine中止,而会等后端响应结束后再返回给客户端。

优化关闭应用并不只是在负载均衡或反向代理层面就能完美解决的,后端的应用本身也要考虑很多细节。举一个例子,Actor怎么有序的关闭,可以参考之前分享的Governor模式,所有干活的Actor由Governor创建,在创建时就设定好优先级。

在应用关闭的时候,对master发送PoisonPill,示例代码如下:

// 在你的容器或微容器里监听到应用关闭事件时触发shutdown,最简单的情况下是在shutdownhook里
def shutdown(): Unit = {
    gracefulStopActors
    system.terminate
    Await.result(system.whenTerminated, Duration.Inf)
    springContext.close
}

private def gracefulStopActors {
    master ! PoisonPill
    signal.acquire //wait
}

在governor角色里,当收到Terminated消息后顺序结束子actor

...
case Terminated(actor) if actor == observable => {
  logger.info("===receive terminated message from: " + actor)
  // 先顺序的杀死所有托管的子actor,并将结果发给自己
  stopAll(managedActors.toList.sorted) pipeTo self
}
...

// 顺序的停止所有子actor
protected def stopAll(kids: List[OrderedActorRef]): Future[Any] = {
    kids match {
      case first :: Nil =>
        logger.info("===graceful stop: " + first.actor)
        gracefulStop(first.actor, stopTimeout).flatMap { _ =>
          Future { AllDead }
      }
      case first :: rest =>
        logger.info("===graceful stop: " + first.actor)
        gracefulStop(first.actor, stopTimeout).flatMap { _ =>
        stopAll(rest)
      }
      case Nil =>
        Future { AllDead }
    }
}

再举一个例子,应用里有一个线程一直轮询从redis里获取数据,你在关闭应用的时候如何优雅终止这个线程呢?下面用一段代码简单模拟这个场景;假设这个应用没有任何容器,结束就是靠收到kill信号,那么在ShutdownHook里,要阻塞的等待(一段时间)worker线程结束后再退出才算安全

public class Test {
  static volatile boolean running = true;

  public static void main(String[] args) {
  Thread worker = new Thread() {
      public void run() {
          while (running) {
              try {
                  System.out.println("i'm running.1");
                  Thread.sleep(1000);
                  System.out.println("i'm running.2");
                  Thread.sleep(1000);
                  System.out.println("i'm running.3");
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  };
  worker.start();
  Runtime.getRuntime().addShutdownHook(new Thread() {
      public void run() {
          System.out.println("receive shutdown signal.");
          running = false;

          while(worker.isAlive()) {
              System.out.println("waiting for worker thread finish.");
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
              }
          }
          System.out.println("wroker thread finished!");
      }
  });
 }
}

shell下精确的定位一个命令

之前在这篇SIGTTIN? 里提出了问题,但没有交代背景,为什么用交互式shell执行”which mvn”命令,是为了更准确的获取用户当前环境里所用的mvn命令到底是哪个。若用户对mvn做过alias,优先使用alias过的指令,然后再选择$PATH路径下的命令;这个函数的完整内容如下:

function get_mvn_cmd() {
  if [[ "$OSTYPE" == *cygwin* ]];then
    ppid=$( ps -ef -p $$ | awk 'NR==2{print $3}' )
    user_shell=$( ps -p $ppid | awk 'NR==2{print $8}' )
    #has some trouble with cygwin, while Ctrl-c cannot terminal
    set -m
  else
    ppid=$( ps -oppid= $$ )
    user_shell=$( ps -ocomm= -p $ppid )
  fi

  # while as login shell, it's -bash not bash
  if [[ "$user_shell" == "-"* ]];then
    user_shell=${user_shell:1}
  fi

  mvn=$( $user_shell -ic "alias mvn" 2>/dev/null | cut -d'=' -f2 | sed "s/'//g" )

  if [ -z "$mvn" ];then
    $user_shell -ic "which mvn" >/dev/null
    if [ $? -eq 0 ];then
      mvn=$( $user_shell -ic "which mvn" | head -1 )
    fi
  fi

  if [ -z "$mvn" ]; then
    echo "mvn command not found" >&2
    kill -s TERM $TOP_PID
  else
    echo $mvn
  fi
}

函数里先获取用户所使用的shell(即当前脚本的父进程),然后以交互式执行alias mvn看看是否又被用户别名过,没有的话再使用which mvn获取mvn命令。

为什么写的那么啰嗦是想兼容好几种用户环境,如果是linux或者使用的gnu-which,可以利用gnu-which的一些参数一次性按优先级依次从”alias, functions, commands”里查找命令

$ (alias; declare -f) | gwhich --read-alias --read-functions mvn

上面利用gnu-which的--read-alias--read-functions参数优先从前边aliasdeclare -f输出的结果里查找,最后再从$PATH里查找。实际上centos7的bash下which命令就被alias过了:

alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'