如何缓冲和处理批量尾部-f输出?



我需要监视一个文件并将写入其中的内容发送到Web服务。我正在尝试使用 bash 脚本实现一个干净简单的解决方案,例如:

#!/bin/bash
# listen for changes on file specified as first argument
tail -F "$1" | while read LINE
do
curl http://service.com/endpoint --data "${LINE}"
done

这非常有效,因为附加的每一行都将被 POST 到 http://service.com/endpoint。 但是,我真的不喜欢这样一个事实,即如果在短时间内附加许多行,我将拥有尽可能多的HTTP请求,并可能使服务过载。

有没有一种聪明的方法来缓冲操作?我可以想到这样的事情:

buffer = EMPTY
while LINES are read:
add LINE to buffer
if buffer has more than X LINES
send POST
fi
done

但是在上面的解决方案中,如果每小时发布一行,我只会每 X 小时获得一次更新,这是不可接受的。另一个类似的解决方案是在 while 循环中"时间":if X seconds have passed then send buffer, otherwise wait..但是流的最后一行可能会无限期地保留,因为 while 循环仅在向文件添加新行时触发。

目标是使用最少的 bash 脚本来做到这一点,并且不使用第二个进程。我所说的第二个过程是指:process 1 gets the output from tail -f and stores itprocess 2 periodically checks what is stored and sends a POST if more than x seconds are elapsed

我很好奇这是否可以通过一些聪明的技巧来实现?

谢谢!

从字面上看,将伪代码放入代码:

# add stdbuf -oL if you care
tail -F "$1" | {
# buffer = EMPTY
buffer=
# while LINES are read:
while IFS= read -r line; do
# add LINE to buffer
buffer+="$line"$'n'
# if buffer has more than X LINES
# TODO: cache the count of lines in a variable to save cpu
if [ $(wc -l <<<"$buffer") -gt "$x_lines" ]; then
# send POST
# TODO: remove additional newline on the end of buffer, if needed
curl http://service.com/endpoint --data "${buffer}"
buffer=
fi
done
}

删除缓冲区末尾的换行符,或者例如在单独的计数器中缓冲行数以节省 CPU 留给其他人。

笔记:

  • 按照惯例,大写变量保留给全局导出的变量。
  • while read LINE将从行中删除前导和尾随空格。使用while IFS= read -r line阅读整行。更多信息 bashfaq 如何逐行读取文件
  • 一行,我相信你可以用xargstail -F "$1" | xargs -d$'n' -n1 curl http://service.com/endpoint --data

为了缓冲时间,请超时读取 - 使用 bash 扩展,例如。read -t 0.1或通过超时整个读取timeout 1 cat

为了在两个方面限制行数和超时,我曾经写过一个名为 ratelimit.sh 的命名错误的脚本(名称不好,因为它不限制速率......(,它正是这样做的。它读取行,如果达到行数或超时,它会使用额外的输出分隔符刷新缓冲区。我相信它应该像tail -F "$1" | ratelimit.sh --timeout=0.5 --lines=5 | while IFS= read -r -d $'x02' buffer; do curl ... --data "$buffer"; done一样使用.它大致是这样工作的:

# Written by Kamil Cukrowski (C) 2020
# Licensed jointly under MIT and Beerware license
# config
maxtimeoutns=$((2 * 1000 * 1000 * 1000))
maxlines=5 
input_separator=$'n'
output_separator=$'x02'
# the script
timeout_arg=()
while true; do
chunk=""
lines=0
start=$(date +%s%N)
stop=$((start + maxtimeoutns))
while true; do
if [ "$maxtimeoutns" != 0 ]; then
now=$(date +%s%N)
if (( now >= stop )); then
break
fi
timeout=$(( stop - now ))
timeout=$(awk -va=$timeout -vb=1000000000 '{print "%f", a/b}' <<<"")
timeout_arg=(-t "$timeout")
fi

IFS= read -rd "$input_separator" "${timeout_arg[@]}" line && ret=$? || ret=$?
if (( ret == 0 )); then
# read succeded
chunk+=$line$'n'
if (( maxlines != 0 )); then
lines=$((lines + 1))
if (( lines >= maxlines )); then
break
fi
fi
elif (( ret > 128 )); then
# read timeouted
break;
fi
done
if (( ${#chunk} != 0 )); then
printf "%s%s" "$chunk" "$output_separator"
fi
done

多亏了KamilCuk的回答,我设法以一种相当简单的方式实现了我想要的东西,结合了最大行数和超时。诀窍是发现管道不一定像我想象的那样按线工作。傻我!

仅供将来参考,这是我的解决方案,它非常具体且简化为骨头:

#!/bin/bash
# sends updates to $1 via curl every 15 seconds or every 100 lines
tail -F "$1" | while true; do
chunk=""
stop=$((`date +%s` + 15))
maxlines=100
while true; do
if (( `date +%s` >= stop )); then break; fi
IFS= read -r -t 15 line && ret=$? || ret=$?         
if (( ret == 0 )); then
chunk+=$line$'n'
maxlines=$((maxlines - 1))
if (( maxlines == 0 )); then break; fi
elif (( ret > 128 )); then break; fi
done
if (( ${#chunk} != 0 )); then
curl http://service.com --data "$chunk";
fi
done

最新更新