• 重定向一个进程的输出

    把ping的输出重定义向文件中

    先跑一个ping进程

    ping www.baidu.com 
    PING www.a.shifen.com (61.135.169.121) 56(84) bytes of data.
    64 bytes from 61.135.169.121: icmp_seq=1 ttl=61 time=26.9 ms
    64 bytes from 61.135.169.121: icmp_seq=2 ttl=61 time=26.3 ms
    64 bytes from 61.135.169.121: icmp_seq=3 ttl=61 time=24.3 ms
    64 bytes from 61.135.169.121: icmp_seq=4 ttl=61 time=23.6 ms
    64 bytes from 61.135.169.121: icmp_seq=5 ttl=61 time=24.7 ms
    64 bytes from 61.135.169.121: icmp_seq=6 ttl=61 time=23.5 ms
    

    到/proc/XX/fd 下面看一下当前所有的文件描述符

    # ls -l
    total 0
    lrwx------ 1 root root 64 Apr 11 09:31 0 -> /dev/pts/0
    lrwx------ 1 root root 64 Apr 11 09:31 1 -> /dev/pts/0
    lrwx------ 1 root root 64 Apr 11 09:31 2 -> /dev/pts/0
    lrwx------ 1 root root 64 Apr 11 09:31 3 -> socket:[179350]
    

    用gdb链接到这个进程中, gdb -p XX, 创建一个新的文件, 并把文件描述符指过去.

    (gdb) p creat("/tmp/ping.out", 0644)
    $1 = 4
    (gdb) p dup2(4,1)
    $2 = 1
    (gdb) p close(4)
    $3 = 0
    (gdb) q
    

    退出gdb之后, 可以看到ping不再输出到屏幕, 而是到/tmp/ping.out中了.

    把ping的输出重定义向到另外一个文件

    先跑一个ping进程, 标准输出指向一个文件

    ping www.baidu.com  > /tmp/ping.out
    

    用gdb链接到这个进程中, gdb -p XX, 创建一个新的文件, 并把文件描述符指过去.

    (gdb) p creat("/tmp/ping.out.2", 0644)
    $1 = 4
    (gdb) p dup2(4,1)
    $2 = 1
    (gdb) p close(4)
    $3 = 0
    (gdb) q
    

    恢复已经删除的文件

    先跑一个ping进程, 重定向到/tmp/ping.out

    ping www.baidu.com > /tmp/ping.out
    

    到/proc/XX/fd 下面看一下当前所有的文件描述符

    root@7d82fa25da6c:/proc/28/fd# ll
    total 0
    dr-x------ 2 root root  0 Apr 11 09:47 ./
    dr-xr-xr-x 9 root root  0 Apr 11 09:47 ../
    lrwx------ 1 root root 64 Apr 11 09:48 0 -> /dev/pts/0
    l-wx------ 1 root root 64 Apr 11 09:48 1 -> /tmp/ping.out
    lrwx------ 1 root root 64 Apr 11 09:47 2 -> /dev/pts/0
    lrwx------ 1 root root 64 Apr 11 09:48 3 -> socket:[180914]
    

    如果不小心删除了 /tmp/ping.out, 其实ping程序还在不停的写磁盘, 只不过看不到了. (tail -f /proc/28/fd/1 还是可以看到当前的输出) 而且磁盘会被不停的使用, 但很难发现是哪些文件在增长. (du是看不到的.)

    如果要清除被这个隐形的文件占用的空间, 只要 echo > /proc/28/fd/1 就可以了.

    但是这个也不怎么治本, 我们需要恢复 /tmp/ping.out这个文件.

    简单的 touch /tmp/ping.out 是没有用的, 还是需要gdb attach过去.

    (gdb) p creat("/tmp/ping.out", 0644)
    $1 = 4
    (gdb) p dup2(4,1)
    $2 = 1
    (gdb) p close(4)
    $3 = 0
    (gdb) q
    

    搞定.

  • 如何保存命令的返回值到一个变量中

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

    如何保存命令的返回值到一个变量中, 这个取决于你是想保存命令的输出,还是他的返回码(0到255, 一般来说0代表成功).

    如果是想捕获输出, 可以用command substitution

    output=$(command)      # stdout only; stderr remains uncaptured
    output=$(command 2>&1) # both stdout and stderr will be captured
    

    如果是想要返回码, 应该在运行命令之后, 用特殊参数 $?

    command
    status=$?
    

    如果两者都需要:

    output=$(command)
    status=$?
    

    如果不是想要返回码, 而只是想知道命令成功还是失败, 可以如下这样

    if command; then
        printf "it succeeded\n"
    else
        printf "it failed\n"
    fi
    

    如果要根据成功/失败执行下一步操作, 但不想知道返回码, 又要取输出内容:

    if output=$(command); then
        printf "it succeeded\n"
        ...
    

    如果想从一个pippline里面获取其中一个command的返回码? 最后一个的话, 就是 $? . 如果是中间某个呢? 用PIPESTATUS数组(只在bash中有效)

    grep foo somelogfile | head -5
    status=${PIPESTATUS[0]}
    

    bash3.0 又添加了一个pipefail选项, 如果你想grep失败的时候执行下一步:

    set -o pipefail
    if ! grep foo somelogfile | head -5; then
        printf "uh oh\n"
    fi
    

    好, 现在来看一些更复杂的问题: 如果只想要错误输出, 而不想要标准输出. 首先, 你需要决定把标准输出指向哪里去.

    output=$(command 2>&1 >/dev/null)  # Save stderr, discard stdout.
    output=$(command 2>&1 >/dev/tty)   # Save stderr, send stdout to the terminal.
    output=$(command 3>&2 2>&1 1>&3-)  # Save stderr, send stdout to script's stderr.
    

    最后一个有些难以理解. 首先要了解 1>&3- 等价于1>&3 3>&-. 然后按下表中的顺序理一下

    Redirection fd 0 (stdin) fd 1 (stdout) fd 2 (stderr) fd 3 Description
    initial /dev/tty /dev/tty /dev/tty   假设命令是跑在一个终端. stdin stdout stder全部都是初始化为指向终端(tty)
    $(…) /dev/tty pipe /dev/tty   标准输出被管道捕获
    3>&2 /dev/tty pipe /dev/tty /dev/tty 把描述符2复制到新建的一个描述3, 这时候描述符3指向标准错误输出
    2>&1 /dev/tty pipe pipe /dev/tty 描述符2指向1当前的指向, 也就是说2和1一起都是被捕获
    1>&3 /dev/tty /dev/tty pipe /dev/tty 复制3到1, 也就是说描述符1指向了标准错误. 到现在为止, 我们已经交换了1和2
    3>&- /dev/tty /dev/tty pipe   最后关闭3, 已经不需要了

    n>&m- 有时候称之为 将m 重命名为n.

    来个更复杂的! 我们把stder保存下来, stdout还是往之前应该去的地方去, 就好像stdout没有做过任何的重定向.

    有两种方式

    exec 3>&1                    # Save the place that stdout (1) points to.
    output=$(command 2>&1 1>&3)  # Run command.  stderr is captured.
    exec 3>&-                    # Close FD #3.
    
    # Or this alternative, which captures stderr, letting stdout through:
    { output=$(command 2>&1 1>&3-) ;} 3>&1
    

    我觉得有必要说明一下带重定向的命令的执行方式. command 2>&1 1>&3 是先把2指向1, 然后把1指向3

    第一种方式应该还比较好懂. 先创建FD3,并把1复制到3, 然后执行命令 $(command 2>&1 1>&3) , 把FD1的输出管道给output, 然后把3关闭.

    第二种方式, 其实就是把三行合成为1行了.

    如果想分别保存stdout, stderr到2个变量中, 只用FD是做不到的. 需要用到一个临时文件, 或者是命名的管道.

    一个很糟糕的实现如下:

    result=$(
        { stdout=$(cmd) ; } 2>&1
            printf "this line is the separator\n"
                printf "%s\n" "$stdout"
                )
    var_out=${result#*this line is the separator$'\n'}
    var_err=${result%$'\n'this line is the separator*}
    

    如果还想保存返回码的话

    cmd() { curl -s -v http://www.google.fr; }
    
    result=$(
        { stdout=$(cmd); returncode=$?; } 2>&1
            printf "this is the separator"
                printf "%s\n" "$stdout"
                    exit "$returncode"
                    )
    returncode=$?
    
    var_out=${result#*this is the separator}
    var_err=${result%this is the separator*}
    

    Done.

  • kafka的这个BUG给我造成了10000点的伤害

    消费kafka数据的时候, 总是有几个partition会停住不消费, 有时候过十分钟, 或者2小时, 又开始消费了…

  • elasticsearch: 返回parent文档, 但是按children里面的值排序

    背景, 需求

    有业务部门要用elasticsearch做酒店搜索, 大概的数据类型如下.

    #parent
    POST indexname/hotel/_mapping
    {
       "properties": {
          "hotelid": {
             "type": "long"
          },
          "name": {
             "type": "string"
          }
       }
    }
    
    #child
    POST indexname/room/_mapping
    {
       "_parent": {
          "type": "hotel"
       },
       "properties": {
          "roomid": {
             "type": "long"
          },
          "price": {
             "type": "long"
          },
          "area": {
             "type": "float"
          }
       }
    }
    

    父类型是酒店, 下面有子类型, 是房间, 每个房间的面积和价格不一样.

    搜索的时候, 结果是展示酒店, 但以每个房间的最小价格排序.

    有一方案是把room做为一个字段(列表)放到hotel里面去, 但这样的话, 每更新一个房间信息, 其实都是更新了整个hotel文档. 所以还是希望做成parent/children结构.

    用has_child搜索方式, 再结合自定义打分功能, 可以实现.

    准备一下数据

    插入几条酒店信息

    POST indexname/hotel/1
    {
       "hotelid": 1,
       "name": "abc"
    }
    POST indexname/hotel/2
    {
       "hotelid": 2,
       "name": "xyz"
    }
    

    再插入几条房间信息

    POST indexname/room/a?parent=1
    {
       "price": 100
    }
    POST indexname/room/b?parent=2
    {
       "price": 200
    }
    POST indexname/room/c?parent=3
    {
       "price": 300
    }
    

    搜索

    POST indexname/hotel/_search
    {
       "query": {
          "has_child": {
             "type": "room",
             "score_mode": "max",
             "query": {
                "function_score": {
                   "query": {},
                   "script_score": {
                      "script": "-1*doc['price'].value"
                   }
                }
             }
          }
       }
    }
    

    搜索结果

    {
       "took": 9,
       "timed_out": false,
       "_shards": {
          "total": 5,
          "successful": 5,
          "failed": 0
       },
       "hits": {
          "total": 2,
          "max_score": -100,
          "hits": [
             {
                "_index": "indexname",
                "_type": "hotel",
                "_id": "1",
                "_score": -100,
                "_source": {
                   "hotelid": 1,
                   "name": "abc"
                }
             },
             {
                "_index": "indexname",
                "_type": "hotel",
                "_id": "2",
                "_score": -200,
                "_source": {
                   "hotelid": 2,
                   "name": "xyz"
                }
             }
          ]
       }
    }
    
  • elasticsearch: transport client bulk的时候如何选择目标node

    背景

    我们之前用Losgtash做indexer把数据从kafka消费插入ES, 所有的数据都是先经过Logstash里面配置的四个client节点, 然后经他们再分配到数据节点.

    后来因为logstash效率太低, 改成我们自己用java开发的的hangout做同样的事情, 发现数据不再走client, 而是直接到数据节点. 原因是构造transport client的时候设置成sniff: true.

    但还是有一个困惑, bulk的一批数据, 可能最终会到多个节点上面索引, 那么是client在发送数据的时候就已经计算好应该把哪些数据发往哪个节点, 还是说随便发到nodeX, 然后nodeX再二次分发.

    碰到这个问题的时候, 我想当然的以为是前者, 因为transport client可以拿到所有的metadata,应该可以算出来怎么分发. 如果是后者的话, 流量要复制一份, 过于浪费了.

    但验证之后, 发现并非如此.

    测试

    1. 建一个有四个节点的集群, 并新建一个索引, 四个shards, 全部分布在一个节点上

       GET hangouttest-2016.03.21/_settings
       {
          "hangouttest-2016.03.21": {
             "settings": {
                "index": {
                   "routing": {
                      "allocation": {
                         "require": {
                            "_ip": "10.2.7.159"
                         }
                      }
                   },
                   "creation_date": "1458570866963",
                   "number_of_shards": "4",
                   "number_of_replicas": "0",
                   "uuid": "FkWPR_WaQpG5LdIABCEzVw",
                   "version": {
                      "created": "2010199"
                   }
                }
             }
          }
       }
      
      

      GET _cat/shards/hangouttest-2016.03.21?v index shard prirep state docs store ip node
      hangouttest-2016.03.21 2 p STARTED 28 28.5kb 10.2.7.159 10.2.7.159 hangouttest-2016.03.21 3 p STARTED 22 27.8kb 10.2.7.159 10.2.7.159 hangouttest-2016.03.21 1 p STARTED 30 28.8kb 10.2.7.159 10.2.7.159 hangouttest-2016.03.21 0 p STARTED 20 27.5kb 10.2.7.159 10.2.7.159 ```

    2. 写代码, 先生成一个transport client, 配置成20条数据bulk一次. 参考https://www.elastic.co/guide/en/elasticsearch/client/java-api/2.2/java-docs-bulk-processor.html

       import org.elasticsearch.action.bulk.BackoffPolicy;
       import org.elasticsearch.action.bulk.BulkProcessor;
       import org.elasticsearch.common.unit.ByteSizeUnit;
       import org.elasticsearch.common.unit.ByteSizeValue;
       import org.elasticsearch.common.unit.TimeValue;
      
       BulkProcessor bulkProcessor = BulkProcessor.builder(
               client,  
               new BulkProcessor.Listener() {
                   @Override
                   public void beforeBulk(long executionId,
                                          BulkRequest request) { 
                       System.out.println("beforeBulk");
                       } 
      
                   @Override
                   public void afterBulk(long executionId,
                                         BulkRequest request,
                                         BulkResponse response) {
                       System.out.println("afterBulk");
                   }
      
                   @Override
                   public void afterBulk(long executionId,
                                         BulkRequest request,
                                         Throwable failure) { ... } 
               })
               .setBulkActions(10000) 
               .setBulkSize(new ByteSizeValue(1, ByteSizeUnit.GB)) 
               .setFlushInterval(TimeValue.timeValueSeconds(5)) 
               .setConcurrentRequests(1) 
               .setBackoffPolicy(
                   BackoffPolicy.exponentialBackoff(TimeValue.timeValueMillis(100), 3)) 
               .build();
      
    3. tcpdump开起来, 抓包分析流量

       % sudo tcpdump -nn 'ip[2:2]>200'
      
    4. 发送四次数据, 每次20条, 每条100字节左右.

    5. 抓包结果, 可以看到四次bulk请求分别发往了四个节点

       % sudo tcpdump -nn 'ip[2:2]>200'
       22:39:16.222034 IP 10.170.30.45.63034 > 10.2.7.165.9300: Flags [P.], seq 1053720918:1053721314, ack 4146795087, win 4128, options [nop,nop,TS val 698503243 ecr 3217616506], length 396
       22:39:19.951115 IP 10.170.30.45.63047 > 10.2.7.159.9300: Flags [P.], seq 2684147573:2684147960, ack 2244520841, win 4128, options [nop,nop,TS val 698506819 ecr 3217621511], length 387
       22:39:23.385240 IP 10.170.30.45.63060 > 10.2.7.168.9300: Flags [P.], seq 318079750:318080148, ack 3392556208, win 4128, options [nop,nop,TS val 698510112 ecr 3217626519], length 398
       22:39:26.688067 IP 10.170.30.45.63021 > 10.2.7.161.9300: Flags [P.], seq 84160388:84160780, ack 4144775292, win 4128, options [nop,nop,TS val 698513218 ecr 3217626516], length 392
      
    6. 源码分析

      选择node的代码在 org.elasticsearch.client.transport.TransportClientNodesService, getNodeNumber就是简单的+1

       public <Response> void execute(NodeListenerCallback<Response> callback, ActionListener<Response> listener) {
           List<DiscoveryNode> nodes = this.nodes;
           ensureNodesAreAvailable(nodes);
           int index = getNodeNumber();
           RetryListener<Response> retryListener = new RetryListener<>(callback, listener, nodes, index);
           DiscoveryNode node = nodes.get((index) % nodes.size());
           try {
               callback.doWithNode(node, retryListener);
           } catch (Throwable t) {
               //this exception can't come from the TransportService as it doesn't throw exception at all
               listener.onFailure(t);
           }
       }
      

      然后回调到org.elasticsearch.client.transport.support.TransportProxyClient:

       public void doWithNode(DiscoveryNode node, ActionListener<Response> listener) {
           proxy.execute(node, request, listener);
       }
      

      后面就是往这个node发送数据了.

    Over.

  • 利用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