• 利用tcpdump和kafka协议定位不合法topic的来源client

    事情缘由

    事情是这样滴, 我们在很多linux机器上部署了logstash采集日志, topic_id用的是 test-%{type}, 但非常不幸的是, 有些机器的某些日志, 没有带上type字段.

    因为在topic名字里面不能含有%字符, 所以kafka server的日志里面大量报错. Logstash每发一次数据, kafka就会生成下面一大段错误

    我想做的, 就是把有问题的logstash机器找出来.

  • 如何按行或者按列读取文件/数据流/变量

    翻译自http://mywiki.wooledge.org/BashFAQ/001

    不要使用for. 要使用while循环和read来实现.

    while IFS= read -r line; do
        printf '%s\n' "$line"
    done < "$file"
    

    read后面的-r选项可以阻止\转义, 如果不用-r, 单独的\会被忽略. 使用read的时候, 几乎一定要跟着-r.

    看一下例子吧,先是用-r

    % cat 01.sh
    file="01.sh"
    while IFS= read -r line; do
        printf '%s\n' "$line"
    done < "$file"
    
    % sh 01.sh
    file="01.sh"
    while IFS= read -r line; do
        printf '%s\n' "$line"
    done < "$file"
    

    把-r去掉看一下

    % cat 01.sh
    file="01.sh"
    while IFS= read line; do
        printf '%s\n' "$line"
    done < "$file"
    
    % sh 01.sh
    file="01.sh"
    while IFS= read line; do
        printf '%sn' "$line"
    done < "$file"
    

    IFS= 是为了避免把前后的空格去掉, 如果你就是想把前后空格去掉, 就不要用IFS= 了.

    IFS是一个很有意思的东西,再多一些了解之后,会记录一下.

    line是一个变量名, 随便你叫什么,可以用任何在shell中合法的变量名

    重定向符号 < “$file” , 告诉while循环从file这个文件中读取内容. 也可以不用变量, 就直接用一个字符串. 像 < 01.sh.

    如果数据源就是标准输入,就不用任何重定向了. (ctrl+D)结束.

    如果输入源是变量或者参数中的内容,bash可以用 «< 遍历数据 (原文中把它叫做here string)

    while IFS= read -r line; do
      printf '%s\n' "$line"
    done <<< "$var"
    

    也可以用 « (原文中把它叫做here document)

    while IFS= read -r line; do
      printf '%s\n' "$line"
    done <<EOF
    $var
    EOF
    

    如果想把#开头的过滤掉, 可以在循环中直接跳过, 如下

    # Bash
    while read -r line; do
      [[ $line = \#* ]] && continue
      printf '%s\n' "$line"
    done < "$file"
    

    如果想对每一列单独处理, 可以在read后用多个变量

    # Input file has 3 columns separated by white space (space or tab characters only).
    while read -r first_name last_name phone; do
      # Only print the last name (second column)
      printf '%s\n' "$last_name"
    done < "$file"
    

    如果分隔符不是空白符, 可以设置IFS, 如下:

    # Extract the username and its shell from /etc/passwd:
    while IFS=: read -r user pass uid gid gecos home shell; do
      printf '%s: %s\n' "$user" "$shell"
    done < /etc/passwd
    

    如果文件是用tag做分隔的, 可以设置IFS=$’\t’, 不过注意了, 多个连着的tab会被当成一个(Ksh93/Zsh中可以用IFS=$’\t\t’, 但Bash中没用)

    如果你给的变量数多于这行中的列数, 多出来的变量就是空, 如果少于列数,最后多出来的所有的数据会写到最后一个变量中

    read -r first last junk <<< 'Bob Smith 123 Main Street Elk Grove Iowa 123-555-6789'
    
    # first will contain "Bob", and last will contain "Smith".
    # junk holds everything else.
    

    也可以使用点位符忽略我们不需要的值

    read -r _ _ first middle last _ <<< "$record"
    
    # We skip the first two fields, then read the next three.
    # Remember, the final _ can absorb any number of fields.
    # It doesn't need to be repeated there.
    

    再次注意, bash中用_肯定是没问题的, 但其他一些shell中, 可能有其它含义,有可能会使脚本完全不能用, 所以最好选一个不会在脚本的其它地方用到的变量替代,以防万一.

    也可以把一个命令的输出做为read的输入:

    some command | while IFS= read -r line; do
      printf '%s\n' "$line"
    done
    

    比如find找到需要的文件后, 将他们重命名,把空格改成下划线.

    find . -type f -print0 | while IFS= read -r -d '' file; do
        mv "$file" "${file// /_}"
    done
    

    注意find里面用到了print0, 是说一个null作为文件名的分隔符; read用了-d选项,也是说用null做分隔符. 默认情况下,它们都是用\n做分隔符的,但文件名本身就有\n时,脚本就出错了. IFS也要设置为空字符串,避免文件名前后有空白符的情况.

    我的文件最后一行没有最后的换行符!

    最后一行不是以\n结尾的话,read会读取之后返回false,所以就跳出了while循环,循环里面是不能输出这最后一行的. 可以这样处理:

    # Emulate cat
    while IFS= read -r line; do
      printf '%s\n' "$line"
    done < "$file"
    [[ -n $line ]] && printf %s "$line"
    

    再看下面这段代码:

    # This does not work:
    printf 'line 1\ntruncated line 2' | while read -r line; do echo $line; done
    
    # This does not work either in bash, but work in zsh:
    printf 'line 1\ntruncated line 2' | while read -r line; do echo "$line"; done; [[ $line ]] && echo -n "$line"
    
    # This works:
    printf 'line 1\ntruncated line 2' | { while read -r line; do echo "$line"; done; [[ $line ]] && echo "$line"; }
    

    第一段显然不会输出最后一行, 但奇怪的是第二行也不会!, 因为while循环是在一个subshell里面的, subshell里面的变量的生命周期只在subshell里面; 第三段就{}强制把while和后面的判断放在一个subshell里面,就OK了.

    注:zsh中,第二行种写法是会输出的。

    也可以用下面这样(我觉得挺有意思的)

    printf 'line 1\ntruncated line 2' | while read -r line || [[ -n $line ]]; do echo "$line"; done
    
  • ping的时候unknown host

    前几天有个前同事出了个题目考我们, 在linux上面ping item.jd.hk, 或者是curl的时候, 报unknown host的错误. 但是windows, mac上面正常. 让我们想一下原因是什么.

    先host看一下,

    % host item.jd.hk       
    item.jd.hk is an alias for *.jd.hk.gslb.qianxun.com.
    *.jd.hk.gslb.qianxun.com has address 106.39.164.182
    *.jd.hk.gslb.qianxun.com has address 120.52.148.32
    

    先是Cname到 *.jd.hk.gslb.qianxun.com, 然后 *.jd.hk.gslb.qianxun.com是指向两个IP. 看起来好像没有问题.

    但ping的时候, 的确会报错

    % ping item.jd.hk
    ping: unknown host item.jd.hk
    

    网上搜索一下ping的源码, 很快就可以定位, 是gethostbyname这个函数返回了Null

    hp = gethostbyname(target);
    if (!hp) {
        (void)fprintf(stderr,
            "ping: unknown host %s\n", target);
        exit(2);
    }
    

    然后找gethostbyname的代码, 这个是glibc的函数, google了好一番,终于找到这里:https://fossies.org/dox/glibc-2.23/gethnamaddr_8c_source.html#l00486 (当前还是2.23, 以后版本更新后, 可能需要相应的修改URL)

    gethostbyname调用了gethostbyname2, gethostbyname2最后是调用了2个函数. 先是querybuf发送一个dns查询的请求,然后getanswer解析dns请求的返回.

    struct hostent *
    gethostbyname (const char *name)
    {
        struct hostent *hp;
    
        if (__res_maybe_init (&_res, 0) == -1) {
            __set_h_errno (NETDB_INTERNAL);
            return (NULL);
        }
        if (_res.options & RES_USE_INET6) {
            hp = gethostbyname2(name, AF_INET6);
            if (hp)
                return (hp);
        }
        return (gethostbyname2(name, AF_INET));
    }
    
    struct hostent *
    gethostbyname2 (const char *name, int af)
    {
        ...
        ...
    
        buf.buf = origbuf = (querybuf *) alloca (1024);
    
        if ((n = __libc_res_nsearch(&_res, name, C_IN, type, buf.buf->buf, 1024,
                        &buf.ptr, NULL, NULL, NULL, NULL)) < 0) {
            if (buf.buf != origbuf)
                free (buf.buf);
            Dprintf("res_nsearch failed (%d)\n", n);
            if (errno == ECONNREFUSED)
                return (_gethtbyname2(name, af));
            return (NULL);
        }
        ret = getanswer(buf.buf, n, name, type);
        if (buf.buf != origbuf)
            free (buf.buf);
        return ret;
    

    通过tcpdump抓包, 可以看到请求正常的发出了, 也收到了正确的返回(tpcumpd里面可以看到完整的记录和解析)

    192.168.0.1.53 > 192.168.0.110.37612: 37604 3/0/0 item.jd.hk. CNAME *.jd.hk.gslb.qianxun.com., *.jd.hk.gslb.qianxun.com. A 120.52.148.32, *.jd.hk.gslb.qianxun.com. A 106.39.164.182 (98)
    

    所以querybuf可以不用看, 直接看getanswer.

    getanswer, 简单来说, 就是解析dns response里面的内容, 一一检查分析.

    如果当前记录是A记录(不明白当前记录是A记录什么意思的,以及看不懂上面的tcpdump内容的, 可以搜索一下dns返回的格式),就会调用name_ok(也就是res_hnok这个函数). 在res_hnok中, 如果认为response中格式有错, 直接跳出解析的while循环, 然后返回Null, 随后gethostbyname也返回Null. (errno在哪里设置的, 找不到了, 印象中那天还找到了).

    res_hnok函数的定义在https://fossies.org/dox/glibc-2.23/res__comp_8c_source.html

    int
    res_hnok(const char *dn) {
        int pch = PERIOD, ch = *dn++;
    
        while (ch != '\0') {
            int nch = *dn++;
    
            if (periodchar(ch)) {
                (void)NULL;
            } else if (periodchar(pch)) {
                if (!borderchar(ch))
                    return (0);
            } else if (periodchar(nch) || nch == '\0') {
                if (!borderchar(ch))
                    return (0);
            } else {
                if (!middlechar(ch))
                    return (0);
            }
            pch = ch, ch = nch;
        }
        return (1);
    }
    

    京东把.jd.hk Cname到了.jd.hk.gslb.qianxun.com, .jd.hk.gslb.qianxun.com又A记录到了IP. 这样的话, gethostbyname在解析到.jd.hk.gslb.qianxun.com的时候, 当做A记录解析, 但第一个字母是*, 报错返回Null.

    实际上, 把一个特定的域名, 比如 item.jd.hk Cname到了*.jd.hk.gslb.qianxun.com,然后再到A记录,也是同样的问题. 本质上就是,dns返回的所有回答里面,A记录的host要符合res_hnok函数的检查. 域名的规范可以参考rfc 1035 2.3.1章节.

    解决方案

    和前同事也确认了一下他们的解决方案, 就是把*.jd.hk指向star.jd.hk.gslb.qianxun.com, star.jd.hk.gslb.qianxun.com再配置A记录.

    后续

    1. 如果配置了.jk.hk A XXX.XXX.XXX.XXX, 可以直接查询.jk.hk这个东西, 也是可以返回这个IP的, 但同样的原因, host tcpdump都可以看到,但ping curl会报错. 如果是查询item.jd.hk, 会返回item.jd.hk A XXX.XXX.XXX.XXX这种回答,完全符合规范.

    2. 我回家之后继续测试的时候, 发现随便找一个host, 比如abcdfadsf.jh.hk,第一次是unknown host,第二次就OK了. 抓包分析发现, 是我的路由器把这条记录缓存了, 第二次ping的时候, 直接返回了如abcdfadsf.jh.hk A XXX.XXX.XXX.

  • 从Kafka的一个BUG学到的TCP Keepalive

    Kafka Server Dead Socket 缓慢堆积

    前段时间, 发现Kafka的死连接数一直上升, 从Kafka server这边看, 到其中一个client有几百个established connection, 但从client那边看, 其实只有1个到2个连接.

    经过测试和搜索之后, 确定了是Kafka的一个BUG, 会在0.8.3版本修复, 目前还没有放出来.

    这是一两个月之前, 有其他用户提出的反馈.

    http://comments.gmane.org/gmane.comp.apache.kafka.user/6899

    https://issues.apache.org/jira/browse/KAFKA-2096

    TCP Socket Keepalive

    其实我们几个月之前就发现了这个问题, 不过当时以为是客户端的配置错误(有很多client不停写一个带%字符的非法topic) 引起的.

    关键是没有足够的知识储备, 如果之前就知道tcp的keep alive机制, 可能会早点反应过来.

    记录一下刚学到的tcp keepalive机制.

    主要参考了http://www.tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO

    摘抄翻译几段吧.

    什么是TCP Keepalive

    当你建立一个TCP连接的时候, 就关联了一组定时器. 这里面有些定时器就是处理keepalive的, 它到达0的时候就会发送探针包到对方,看这个socket是不是还活着.探针包不包含数据, ACK标志为1. 不需要对方的socket也配置tcp keepalive.

    如果收到对方回应,就可以断定这个连接还活着. 如果没有回应,可以断定连接已经不能用了, 可以不再维护了.

    为啥需要keepalive

    检查死连接

    这个过程是非常有用的,因为一个所谓的TCP连接其实并没有任何一个东西连着两边. 如果一方突然断电了,另一方是不会知道的.或者中间某个路由表改了,又或者加了一个防火墙, 连接的两方是不会知道的.所以keepalive机制可以去除一些这样的死连接.

    防止因为连接不活动而被中断

    如果连接在一个NAT或者是防火墙后面, 那么一定时间不活动可能会被不经通知就断开.

    NAT或者防火墙会记录经过他们的连接,但能记录的条数总归有个上限. 最普遍也是最合理的策略就是丢弃老的不活动的连接.

    周期的发送一个探针可以把这个风险降低.

    Linux下使用keepalive

    linux内建支持keepalive. 有三个相关的参数.

    1. tcp_keepalive_time

      上次发送数据(简单的ACK不算)多久之后开始发送探针. 默认是2小时.

      当连接被标记为需要keepalive之后, 这个计数器就不再需要了(没理解啥意思)

    2. tcp_keepalive_probes

      一共发多久次探针, 默认9次.

    3. tcp_keepalive_intvl

      两个探针之间隔多久, 默认75秒

    注意, keepalive默认是不启用的,除非在建立socket的时候用setsockopt接口配置了这个socket. 过会给示例.

    如何配置参数

    有两个方法可以配置这三个参数

    proc文件系统

    查看当前值:

      # cat /proc/sys/net/ipv4/tcp_keepalive_time
      7200
    
      # cat /proc/sys/net/ipv4/tcp_keepalive_intvl
      75
    
      # cat /proc/sys/net/ipv4/tcp_keepalive_probes
      9
    

    更改配置:

      # echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time
    
      # echo 60 > /proc/sys/net/ipv4/tcp_keepalive_intvl
    
      # echo 20 > /proc/sys/net/ipv4/tcp_keepalive_probes
    

    sysctl命令

    查看:

    # sysctl \
      > net.ipv4.tcp_keepalive_time \
      > net.ipv4.tcp_keepalive_intvl \
      > net.ipv4.tcp_keepalive_probes
      net.ipv4.tcp_keepalive_time = 7200
      net.ipv4.tcp_keepalive_intvl = 75
      net.ipv4.tcp_keepalive_probes = 9
    

    更改配置:

      # sysctl -w \
      > net.ipv4.tcp_keepalive_time=600 \
      > net.ipv4.tcp_keepalive_intvl=60 \
      > net.ipv4.tcp_keepalive_probes=20
      net.ipv4.tcp_keepalive_time = 600
      net.ipv4.tcp_keepalive_intvl = 60
      net.ipv4.tcp_keepalive_probes = 20
    

    sysctl系统调用

    proc文件系统是内核在用户层的暴露, sysctl命令是对其进程操作的一个接口, 但它不是用的sysctl这个系统调用.

    如果没有proc文件系统可以用, 这时候就要用sysctl系统调用来更改参数了. (应该就是写程序了吧~,具体怎么使用, man好了)

    参数持久化

    如果让更改后的参数在重启后依然有效?

    一般来说, 就是把上面的命令写到启动脚本里面去. 有三个地方可以写.

    1. 配置网络的地方
    2. rc.local脚本
    3. /etc/sysctl.conf sysctl -p会加载/etc/sysctl.conf里面的配置,但请确保启动脚本里面会执行sysctl -p.

    更改的参数也会对已经建立的连接生效.

    写程序时启用Keepalive

    前面提到过, 默认是没有启用这个功能的. 需要在程序中对socket配置一下.

    需要使用这个函数:

    int setsockopt(int s, int level, int optname,
                     const void *optval, socklen_t optlen)
    

    第一个参数就是之前用socket函数得到的socket, 第二个参数一定要是SOL_SOCKET, 第三个参数一定要是SO_KEEPALIVE, 第四个参数是boolean int, 一般就是0或1吧. 第五个参数是第四个参数的大小. 后面有代码示例.

    前面提到的三个参数也可以对一个单独的socket应用, 会覆盖全局的参数.

    • TCP_KEEPCNT: overrides tcp_keepalive_probes

    • TCP_KEEPIDLE: overrides tcp_keepalive_time

    • TCP_KEEPINTVL: overrides tcp_keepalive_intvl

    代码示例:

    
                /* --- begin of keepalive test program --- */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    
    int main(void);
    
    int main()
    {
       int s;
       int optval;
       socklen_t optlen = sizeof(optval);
    
       /* Create the socket */
       if((s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
          perror("socket()");
          exit(EXIT_FAILURE);
       }
    
       /* Check the status for the keepalive option */
       if(getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen) < 0) {
          perror("getsockopt()");
          close(s);
          exit(EXIT_FAILURE);
       }
       printf("SO_KEEPALIVE is %s\n", (optval ? "ON" : "OFF"));
    
       /* Set the option active */
       optval = 1;
       optlen = sizeof(optval);
       if(setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen) < 0) {
          perror("setsockopt()");
          close(s);
          exit(EXIT_FAILURE);
       }
       printf("SO_KEEPALIVE set on socket\n");
    
       /* Check the status again */
       if(getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen) < 0) {
          perror("getsockopt()");
          close(s);
          exit(EXIT_FAILURE);
       }
       printf("SO_KEEPALIVE is %s\n", (optval ? "ON" : "OFF"));
    
       close(s);
    
       exit(EXIT_SUCCESS);
    }
    
                /* ---  end of keepalive test program  --- */
    

    为第三方程序启用keepalive

    如果别人的程序里面没有显式的启用keepalive, 你又想用, 怎么办呢? 两个办法.

    1. 改它的代码, 重新编译运行.
    2. 用libkeepalive这个项目. 它其实是在动态链接库里面更改了socket方法, 在socket之后就自动帮你调用了setsockopt. 所以如果可执行程序是用gcc -static编译出来的, 这个项目就帮不了你了.

    使用libkeepalive的一个例子:

      $ test
      SO_KEEPALIVE is OFF
      
      $ LD_PRELOAD=libkeepalive.so \
      > KEEPCNT=20 \
      > KEEPIDLE=180 \
      > KEEPINTVL=60 \
      > test
      SO_KEEPALIVE is ON
      TCP_KEEPCNT   = 20
      TCP_KEEPIDLE  = 180
      TCP_KEEPINTVL = 60
    

    还有个疑惑

    最后再提一下, 我们的kafka client是启用了keepalive的, 为什么Server那边还有这么多死连接呢??

    由抓包可以确认, 2h之后client的确是发送了探针包, 但奇怪的是server并没有收到,也抓包确认了.

    能想到的一个原因是, 防火墙设置的超时时间小于2小时, 所以等到发送探针的时候连接已经断了, 包直接被防火墙丢弃.

    但实际上不是这样的, 因为在1h50m左右的时候, 尝试发消息还是通的.

    而且写了一个程序, 模拟(伪造)一个TCP包发送到kafka server. 结果是在某个网段发送时, server抓包抓到, 并回复了reset. 但在另外一个网段, server根本就没收到.

    明天再继续跟进, 查一下为啥吧. 大概也是中间某个防火墙的策略把它丢弃了吧?

    PS:确定过了, 中间有防火墙, 30分钟闲置会把网络断开. 后面的包直接被丢弃. 前面提到的1h50m还能发消息是哪里测试错了.

  • 在vim中输入特定范围的IP地址

    在ansible的hosts中需要输入从192.168.0.100到192.168.1.150, 对bash不熟悉, 之前结合python是可以做到的. 刚刚还是搜索了一下bash的方法,记录一下.

    bash的:

    :r !for i in {100..150}; do echo 192.168.1.$i ; done
    

    r就是read的缩写,将后面的内容读入到当前文档.
    如果read filename , 就是把filename里面的内容读入当前文档.

    !代表后面是bash命令, 加起来就是把后面的bash的输出读入当前文档.

    主要是为了纪录一下bash里面对for的这种应用.

    python版本的,注意转义

    :r !python -c "for i in range(100,151):print '192.168.1.\%d'\%i"
    

    learn from http://tldp.org/LDP/abs/html/bashver3.html

  • 淘宝dns解析错误导致首页打不开

    今天下午3点左右吧, 打开淘宝首页的时候被转到一个错误页面, 说我访问的页面不存在.

    看被转过去的页面域名还是err.taobao.com, 所以应该还是淘宝内部的”正常”的跳转, 不是病毒啊什么的.

    开tcpdump抓包看一下. 和淘宝有关的记录如下:

    192.168.0.110.50874 > 101.226.178.141.80: Flags [.], cksum 0x193e (correct), seq 3469495610:3469497050, ack 3942073739, win 4096, options [nop,nop,TS val 479275520 ecr 780673144], length 1440
        D...U\T&..n...E...i.@.@......ne......P..M:..E......>.....
        ..*... xGET / HTTP/1.1
        Host: www.taobao.com
        Connection: keep-alive
        Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
        User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36
        Accept-Encoding: gzip, deflate, sdch
        Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
    
        ...
    
        101.226.178.141.80 > 192.168.0.110.50874: Flags [P.], cksum 0x4485 (correct), seq 1:450, ack 1590, win 126, options [nop,nop,TS val 780733766 ecr 479275520], length 449
        T&..n.D...U\..E.....@.8...e......n.P....E...Sp...~D......
        F..*.HTTP/1.1 302 Found
        Server: Tengine
        Date: Sat, 14 Mar 2015 07:25:11 GMT
        Content-Type: text/html
        Content-Length: 258
        Connection: keep-alive
        Location: http://err.taobao.com/error1.html
    
        <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
        <html>
        <head><title>302 Found</title></head>
        <body bgcolor="white">
        <h1>302 Found</h1>
        <p>The requested resource resides temporarily under a different URI.</p>
        <hr/>Powered by Tengine</body>
        </html>
    
    

    总的来说就是我淘宝www.taobao.com, 域名被解析到101.226.178.141这个IP,然后我的请求被转到了http://err.taobao.com/error1.html这个页面.

    看一下dns:

    % nslookup www.taobao.com                                             127 ↵ ✭
    Server:192.168.0.1
    Address:192.168.0.1#53
    
    Non-authori         tative answer:
    Name:www.taobao.com
    Address: 222.73.134.41
    Name:www.ta     obao.com
    Address: 101.226.178.151
    Name:www.taobao.com
    Address: 222.73.    134.51
    Name:www.taobao.com
    Address: 101.226.178.141    
    

    在 /etc/hosts里添加一行 222.73.134.41 www.taobao.com 指向另外一个IP试一下, 访问正常了. 然后再手工换成101.226.178.141, 还是被转到错误页, 看来101.226.178.141这个IP是有问题.

    网上搜索了一下, 101.226.178.141 这个IP是天猫的. 本地看一下:

    % host www.tmall.com
    www.tmall.com is an alias for www.gslb.taobao.com.danuoyi.tbcache.com.
    www.gslb.taobao.com.danuoyi.tbcache.com has address 101.226.178.111
    www.gslb.taobao.com.danuoyi.tbcache.com has address 101.226.178.141
    www.gslb.taobao.com.danuoyi.tbcache.com has address 101.226.178.151
    www.gslb.taobao.com.danuoyi.tbcache.com has address 101.226.181.111
    www.gslb.taobao.com.danuoyi.tbcache.com has address 222.73.134.41
    www.gslb.taobao.com.danuoyi.tbcache.com has address 222.73.134.51
    www.gslb.taobao.com.danuoyi.tbcache.com has address 101.226.181.101
    www.gslb.taobao.com.danuoyi.tbcache.com has address 101.226.178.101
    

    所以说, 可能是淘宝DNS管理人员不小心把www.taobao.com的一条IP不小心转到天猫去了. 然后还被电信等dns服务器缓存了起来.

    更新: 这其实已经是昨天发生事情了. 但现在看到的dns好像还不对,有可能还是电信的缓存?? 这么久?

    到http://tool.chinaz.com/dns 查看了一下, 上海电信的www.taobao.com的dns地址已经没有101.226.178.141了

    上海[电信]
    101.226.178.151 [上海市 浙江淘宝网络有      限公司电信节点]
    101.226.181.101 [上海市 浙江淘宝网络有限公司电信节点]
    101.226.178.141 [上海市 浙江淘宝网络有限公司电信节点]
    101.226.181.111 [上海市 浙江淘宝网络有限公司电信节点]
    
  • different formatter depends on logging level

    Python中记录日志的时候, 希望不同level的日志使用不同的format, 比如INFO以上级别的, 就简单记录一下msg, DEBUG的还要记录一下文件名, 函数名, 行号等信息.

    但是翻阅了一下官方文档, 以及搜索了不少资料, 都没能简单的找到答案, 最后一点点线索拼凑出来了我需要的效果.

    import logging
    import logging.handlers
    import logging.config
    
    def initlog():
        class MyFormatter(logging.Formatter):
    
            def format(self, record):
                dformatter = '%(levelname)s: [%(asctime)s] - %(pathname)s %(lineno)d - %(message)s'
                formatter = '%(levelname)s: [%(asctime)s] - %(message)s'
                if record.levelno <= logging.DEBUG:
                    self._fmt = dformatter
                else:
                    self._fmt = formatter
                return super(MyFormatter, self).format(record)
    
        config = {
            "version": 1,
            "disable_existing_loggers": True,
            "formatters": {
                "custom": {
                    '()': MyFormatter,
                }
            },
            "handlers": {
                "console": {
                    "class": "logging.StreamHandler",
                    "level": "DEBUG",
                    "formatter": "custom",
                    "stream": "ext://sys.stdout"
                },
                "file_handler": {
                    "class": "logging.handlers.RotatingFileHandler",
                    "level": "DEBUG",
                    "formatter": "custom",
                    "filename": "app.log"
                }
            },
            'root': {
                'level': level,
                'handlers': ['console']
            },
            "loggers": {
                "myloger": {
                    "level": level,
                    "handlers": [
                        "file_handler"
                    ]
                }
            }
        }
        logging.config.dictConfig(config)
    
    
    initlog()
    logging.debug("debug") # write to console
    logger = logging.getLogger("myloger")
    logger.debug("debug") # write to console and file
    logger.error("error") # write to console and file
    
    

    如果需要传自定义参数到MyFormatter里面, 可参考

        class MyFormatter(logging.Formatter):
    
            def __init__(self, args):
                super(MyFormatter, self).__init__()
                print args
    
            def format(self, record):
                dformatter = '%(levelname)s: [%(asctime)s] - %(pathname)s %(lineno)d - %(message)s'
                formatter = '%(levelname)s: [%(asctime)s] - %(message)s'
                if record.levelno <= logging.DEBUG:
                    self._fmt = dformatter
                else:
                    self._fmt = formatter
                return super(MyFormatter, self).format(record)
    
            ....
    
            "formatters": {
                "custom": {
                    '()': MyFormatter,
                    'args': 1
                },
    
            ....
    
  • python中的字符串intern机制

    intern

    python里面, 所有东西都是对象.

    a = {"name":"childe"}
    b = a
    a is b
    

    上面的代码是生成了一个对象, a b 两个变量都指向这个对象.

    a = {"name":"childe"}
    b = {"name":"childe"}
    a is not b
    

    上面的代码是生成了两个对象, a b 两个变量虽然值一样, 但他们是两个不同的对象, 占两块内存.

    对于数字有点不一样, python启动的时候, 已经为M以下的小数字预先分配了内存, 因为python认为这些数字是经常被用到的, 频繁的创建和销毁会浪费资源. M的范围是[-5, 257)

    字符串又有点不一样
    字符串不是python启动时就初始化好的, 是代码中a=“a”的时候时候创建的. 但python会把创建的字符串放在一个缓冲池里面. 之后创建相同字符串的时候, 就会直接返回. 所以x =“a”; y =“a” xy是同一个对象, 也就是 x is y
    这就是intern的概念

    看一下现象

    但是对于字符串, 还是遇到了一些”奇怪”的现象.

    s1="abcd"
    s2="abcd"
    print s1 is s2
    
    
    s1 ="abc"+"d"
    s2="ab"+"cd"
    print s1 is s2
    
    
    s1 ="a"*20
    s2 ="a"*20
    print s1 is s2
    
    
    s1 ="a"*21
    s2 ="a"*21
    print s1 is s2
    
    
    s1 =''.join(["abc","d"])
    s2 =''.join(["ab","cd"])
    print s1 is s2
    
    
    s1 =''.join(["a"])
    s2 =''.join(["a"])
    print s1 is s2
    

    运行结果

    True
    True
    True
    False
    False
    True
    

    what happened

    乍一看, 5段代码, 有时候True 有时候False, 好像很乱, 找不到规律.

    字节码

    python会把脚本翻译成字节码然后再一条条的执行, 我们看一下上面的代码会翻译成什么字节码?就会发现清楚很多了.

    python -m dis phenomena.py
    

    第一段给s1 s2赋值的字节码是这样的

     4           0 LOAD_CONST               0 ('abcd')
                  3 STORE_NAME               0 (s1)
    
      5           6 LOAD_CONST               0 ('abcd')
                  9 STORE_NAME               1 (s2)
    

    这个很容易理解, 但实际上第二段第三段代码也是同样的字节码.

    也就是说 s1 =”abc”+”d” 和 s1 =”a”*20 这样的代码在编译成字节码的时候已经把右边的值编译出来了.

    有点不一样

    但是第四段代码并不是这样, 来对比一下看

     14          46 LOAD_CONST              12 ('aaaaaaaaaaaaaaaaaaaa')
                 49 STORE_NAME               0 (s1)
    
     15          52 LOAD_CONST              13 ('aaaaaaaaaaaaaaaaaaaa')
                 55 STORE_NAME               1 (s2)
    
     19          69 LOAD_CONST               5 ('a')
                 72 LOAD_CONST               7 (21)
                 75 BINARY_MULTIPLY
                 76 STORE_NAME               0 (s1)
    
     20          79 LOAD_CONST               5 ('a')
                 82 LOAD_CONST               7 (21)
                 85 BINARY_MULTIPLY
                 86 STORE_NAME               1 (s2)
    

    why

    记得经常出现的pyc文件吧, 他们实际上就是存储的编译好的字节码.

    “a”20 这样的代码转成”aaaaaaaaaaaaaaaaaaaa”写到字节码的好处显而易见,加快速度,节省内存.
    但如果代码里面有个 “a”
    200000 还编码出来存到pyc, 但pyc就被搞爆掉了, 所以如果字符串长度大于20, 就不会再事先编译好了.

    runtime

    第四,五段代码又有些不一样. ““.join(list) 其实是调用了str的一个方法了, python没有这么牛逼, 直接把方法的执行结果都在编译节段帮我们算出来了, 这个是在runtime执行的. 所以第四段返回了false

    长度为1的字符串

    第五段代码也是执行了join, 而且在字节码中我们看到和第四段是一样的, 那为什么返回了true呢?

    我们来看一下python的代码 http://svn.python.org/projects/python/trunk/Objects/stringobject.c

    join生成新的字符串的时候调用了PyString_FromString函数(我看名字猜的TT..), 里面有段代码如下:

    if (size == 0) {
            PyObject *t = (PyObject *)op;
            PyString_InternInPlace(&t);
            op = (PyStringObject *)t;
            nullstring = op;
            Py_INCREF(op);
        } else if (size == 1 && str != NULL) {
            PyObject *t = (PyObject *)op;
            PyString_InternInPlace(&t);
            op = (PyStringObject *)t;
            characters[*str & UCHAR_MAX] = op;
            Py_INCREF(op);
        }
    

    长度为1的字符串在生成之后会调用PyString_InternInPlace, 这个函数里面实现了intern. 就是到一个全局的字典里面找字符串是不是存在了, 如果已经存在了, 就指过去.

    一点困惑

    再看一下第一段代码的字节码

      4           0 LOAD_CONST               0 ('abcd')
                  3 STORE_NAME               0 (s1)
    
      5           6 LOAD_CONST               0 ('abcd')
                  9 STORE_NAME               1 (s2)
    

    困惑这两次定义是不是调用了两次PyString_FromString, 如果是的话, 并没有哪里做intern的逻辑. 现在觉得其实是只调用了一次, 至于字节码如何被编译成二进制并运行的, 为什么是只调用了一次, 以后再深究吧.

    一个验证的方法就是自己在PyString_FromString中打印一次计数, 然后编译自己的python跑一下.

    参考

    Python中的字符串驻留
    The internals of Python string interning

  • 红绿灯

    这是什么鬼

    有天冲到十字路口, 可是刚刚变成红灯,等了20秒. 接下来还有十几个路口, 就想这次红灯等了20秒, 会不会一连串的红灯把这20秒放大了, 导致到最后耽误了几分钟.有点像蝴蝶效应.

    一直感观认为第一个路口耽误的时间会放大, 不知道有没有别人和我一样的第一感?

    写了一个程序验证了一下, 因为和第一感不一样, 所以也不确认程序模拟的对不对…

    初始化20个红绿灯, 随机红灯时长和绿灯时长. 过马路需要6秒.

    两组做为对比, 一个是当前就在第一个路口, 另一个做对比的是20秒后到了第一个路口.

    和我之前想的不一样, 迟到的人很快就在下一个路口或者是再下个路口和与另一组的人追平了. 极少极少有时间被放大的情况(以至于我根本没有观察到这种情况)

    大规模模拟下来, 平均来看, 在第一个路口晚N秒完全不影响20个路口之后的最终到达时间.

    模拟结果

    模拟的结果如下, 20个红绿灯, 过马路都是6秒, 结果第一行代表程序开始就在第一个路口, 第二行代表等了一秒才到路口, 20行代表过了20秒才到第一个路口. 考虑到第一个路口之前多出的时间, 最后一行仅比第一行多用了20秒不到(有没有哪里搞错了?)

    % python testTrafficLight.py -c 20 -w 6 -f 20 -cmd test
    433.329
    435.082
    436.183
    437.105
    437.728
    438.53
    440.029
    441.116
    441.263
    441.538
    442.96
    443.805
    445.111
    446.188
    448.241
    449.592
    450.386
    451.389
    452.043
    453.071
    

    来看一个具体的例子

    “随机”生成5个路口. 假设每个路口都需要6秒. 我们是第0秒就赶到了第一个路口可以马上过红绿灯了. 结果如下:

    [[27, 67, 72], [34, 47, 36], [56, 64, 11], [21, 66, 38], [62, 20, 36]]  #随机生成的路口
    0 [[27, 67, 72], [34, 47, 36], [56, 64, 11], [21, 66, 38], [62, 20, 36]] # 第0秒出现在第1个路口
    wait_time: 6 #过马路用了6秒
    6 [[27, 67, 78], [34, 47, 42], [56, 64, 17], [21, 66, 44], [62, 20, 42]] # 现在在第二个路口了
    wait_time: 6
    12 [[27, 67, 84], [34, 47, 48], [56, 64, 23], [21, 66, 50], [62, 20, 48]]
    wait_time: 39
    51 [[27, 67, 29], [34, 47, 6], [56, 64, 62], [21, 66, 2], [62, 20, 5]]
    wait_time: 25
    76 [[27, 67, 54], [34, 47, 31], [56, 64, 87], [21, 66, 27], [62, 20, 30]]
    wait_time: 38
    114
    

    “随机”生成和之前一样的5个路口. 同样每个路口都需要6秒. 我们是20秒之后才赶到了第一个路口, 很不巧绿灯不到6秒了, 只能等红灯. 结果如下:

    [[27, 67, 72], [34, 47, 36], [56, 64, 11], [21, 66, 38], [62, 20, 36]] #随机生成的路口
    20 [[27, 67, 92], [34, 47, 56], [56, 64, 31], [21, 66, 58], [62, 20, 56]] # 20秒后出现在第1个路口
    wait_time: 35 #过马路用了35秒
    55 [[27, 67, 33], [34, 47, 10], [56, 64, 66], [21, 66, 6], [62, 20, 9]] # 现在在第二个路口了
    wait_time: 30
    85 [[27, 67, 63], [34, 47, 40], [56, 64, 96], [21, 66, 36], [62, 20, 39]]
    wait_time: 6
    91 [[27, 67, 69], [34, 47, 46], [56, 64, 102], [21, 66, 42], [62, 20, 45]]
    wait_time: 6
    97 [[27, 67, 75], [34, 47, 52], [56, 64, 108], [21, 66, 48], [62, 20, 51]]
    wait_time: 17
    114
    

    可以看到第一个人前2个路口都过得很快, 没有等, 只用了12秒. 但在后面3个路口不巧遭遇了红灯, 被第2个人赶上了..

    完整代码

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import random
    import argparse
    from copy import deepcopy
    
    
    def init(c, maxred, minred, maxgreen, mingren):
        lights = []
        for i in range(c):
            green_time = random.randint(minred, maxred)
            #green_time = 20
            red_time = random.randint(mingren, maxgreen)
            #red_time = 75
            light = [green_time, red_time, random.randint(0, green_time+red_time)]
            lights.append(light)
        return lights
    
    
    def evaluate(f, w, lights):
        all_wait_time = f
        for l in lights:
            l[-1] = (l[-1]+f) % (sum(l[:2]))
    
        for i in range(len(lights)):
    
            light = lights[i]
    
            if light[-1] < light[0]:  # red
                wait_time = light[0]-light[-1]+w
            elif light[-1]+w > light[0]+light[1]: #green time is not enough
                wait_time = light[0]+light[1]-light[-1]+light[0]+w
            else:
                wait_time = w
    
            all_wait_time += wait_time
    
            for l in lights:
                l[-1] = (l[-1]+wait_time) % (l[0]+l[1])
    
        return all_wait_time
    
    
    def main():
        random.seed(1)
        lights = init(
            args.c,
            args.maxred,
            args.minred,
            args.maxgreen,
            args.mingren)
    
        print evaluate(args.f, args.w, lights)
    
    
    def test():
        s = [0]*args.f
    
        for i in range(1000):
            lights = init(
                args.c,
                args.maxred,
                args.minred,
                args.maxgreen,
                args.mingren)
            for j in range(args.f):
                all_wait_time = evaluate(j, args.w, deepcopy(lights))
                s[j] += all_wait_time
    
        for idx,e in enumerate(s):
            print 1.0*e/1000
    
    
    if __name__ == '__main__':
        parser = argparse.ArgumentParser()
        parser.add_argument("-cmd", default="main")
        parser.add_argument("-l", default="DEBUG")
        parser.add_argument(
            "-c",
            type=int,
            default=10,
            help="how many traffic lights,default 10")
        parser.add_argument(
            "-f",
            type=int,
            default=10,
            help="how long should the first light take")
        parser.add_argument(
            "-w",
            type=int,
            default=5,
            help="how long should one light take")
        parser.add_argument("-maxred", type=int, default=75)
        parser.add_argument("-minred", type=int, default=20)
        parser.add_argument("-maxgreen", type=int, default=75)
        parser.add_argument("-mingren", type=int, default=20)
        args = parser.parse_args()
    
        eval(args.cmd)()
    
  • tcp/ip协议学习 第六章 ICMP:Internet控制报文协议

    关于ICMP的RFC文档在此!

    干嘛的

    在我看来, ICMP协议主要就是为了互相传递/查询一些基本信息, 大部分是传递一些错误信息.

    比如A发送UDP信息到B的10000端口, 但B的10000端口并没有开放, 就会回一个ICMP包给A, 告诉A10000端口未开放.

    基本的查询信息, 比如最常用的ping命令, 就是发送ICMP包到目的主机, 然后等待目的主机的响应(响应也是ICMP包).

    协议

    协议定义的非常简单. ICMP在IP层上面一层. 前面是20个字节的IP头, 然后就是ICMP头.

    ICMP头, 截图来自TCP/IP协议详解卷一 ICMP头, 截图来自TCP/IP协议详解卷一

    类型和代码两个字段的组合决定了这个ICMP包的用途, 比如我们常用的ping就是0,0组合和8,0组合. 具体如下:

    各种类型的ICMP报文, 截图来自TCP/IP协议详解卷一 各种类型的ICMP报文, 截图来自TCP/IP协议详解卷一

    代码放上

    好像没有什么好说的. 直接代码放上吧. 实现了一下书中的例子, 一个是查询子网掩码, 一个是查询时间. github地址, 点我点我

    端口不可达

    这也是书中的一个例子. 比如A发送UDP信息到B的10000端口, 但B的10000端口并没有开放, 就会回一个ICMP包给A, 告诉A10000端口未开放.
    来看一下效果.

    用瑞士军刀发送个UDP消息到192.168.0.108的10000端口.

    % nc -u 192.168.0.108 10000                                                   ✭
    abcd
    

    同时开一个Tcpdump监听看看:

    # tcpdump -vvv -x -nn icmp                                                    ✭
    tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
    01:48:01.420363 IP (tos 0x0, ttl 64, id 45430, offset 0, flags [DF], proto ICMP (1), length 56)
        192.168.0.108 > 192.168.0.104: ICMP 192.168.0.108 udp port 10000 unreachable, length 36
    	IP (tos 0x0, ttl 64, id 42558, offset 0, flags [DF], proto UDP (17), length 33)
        192.168.0.104.60181 > 192.168.0.108.10000: [no cksum] UDP, length 5
    	0x0000:  4500 0038 b176 4000 4001 072a c0a8 006c
    	0x0010:  c0a8 0068 0303 eac9 0000 0000 4500 0021
    	0x0020:  a63e 4000 4011 1269 c0a8 0068 c0a8 006c
    	0x0030:  eb15 2710 000d 0000
    

    ping

    代码还是放在github了.