-
重定向一个进程的输出
把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,应该可以算出来怎么分发. 如果是后者的话, 流量要复制一份, 过于浪费了.
但验证之后, 发现并非如此.
测试
-
建一个有四个节点的集群, 并新建一个索引, 四个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 ``` -
写代码, 先生成一个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(); -
tcpdump开起来, 抓包分析流量
% sudo tcpdump -nn 'ip[2:2]>200' -
发送四次数据, 每次20条, 每条100字节左右.
-
抓包结果, 可以看到四次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 -
源码分析
选择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记录.
后续
-
如果配置了.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这种回答,完全符合规范.
-
我回家之后继续测试的时候, 发现随便找一个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. 有三个相关的参数.
-
tcp_keepalive_time
上次发送数据(简单的ACK不算)多久之后开始发送探针. 默认是2小时.
当连接被标记为需要keepalive之后, 这个计数器就不再需要了(没理解啥意思)
-
tcp_keepalive_probes
一共发多久次探针, 默认9次.
-
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_probessysctl命令
查看:
# 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 = 20sysctl系统调用
proc文件系统是内核在用户层的暴露, sysctl命令是对其进程操作的一个接口, 但它不是用的sysctl这个系统调用.
如果没有proc文件系统可以用, 这时候就要用sysctl系统调用来更改参数了. (应该就是写程序了吧~,具体怎么使用, man好了)
参数持久化
如果让更改后的参数在重启后依然有效?
一般来说, 就是把上面的命令写到启动脚本里面去. 有三个地方可以写.
- 配置网络的地方
- rc.local脚本
- /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, 你又想用, 怎么办呢? 两个办法.
- 改它的代码, 重新编译运行.
- 用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 ; doner就是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